blob: 8e9f317e693301bf20fac5c31c4d00350c5f1aaa [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
14import sys
15import re
16import os
17import os.path
18import json
19import argparse
20import logging
Laszlo Nagy258ff252017-02-14 10:43:38 +000021import tempfile
Laszlo Nagybc687582016-01-12 22:38:41 +000022import subprocess
23import multiprocessing
Laszlo Nagy258ff252017-02-14 10:43:38 +000024import contextlib
25import datetime
Laszlo Nagy52c1d7e2017-02-14 10:30:50 +000026from libscanbuild import initialize_logging, tempdir, command_entry_point, \
27 run_build
Laszlo Nagybc687582016-01-12 22:38:41 +000028from libscanbuild.runner import run
29from libscanbuild.intercept import capture
Laszlo Nagy258ff252017-02-14 10:43:38 +000030from libscanbuild.report import document
Laszlo Nagybc687582016-01-12 22:38:41 +000031from libscanbuild.clang import get_checkers
Laszlo Nagy8bd63e52016-04-19 12:03:03 +000032from libscanbuild.compilation import split_command
Laszlo Nagybc687582016-01-12 22:38:41 +000033
34__all__ = ['analyze_build_main', 'analyze_build_wrapper']
35
36COMPILER_WRAPPER_CC = 'analyze-cc'
37COMPILER_WRAPPER_CXX = 'analyze-c++'
38
39
40@command_entry_point
41def analyze_build_main(bin_dir, from_build_command):
42 """ Entry point for 'analyze-build' and 'scan-build'. """
43
44 parser = create_parser(from_build_command)
45 args = parser.parse_args()
46 validate(parser, args, from_build_command)
47
48 # setup logging
49 initialize_logging(args.verbose)
50 logging.debug('Parsed arguments: %s', args)
51
52 with report_directory(args.output, args.keep_empty) as target_dir:
53 if not from_build_command:
54 # run analyzer only and generate cover report
55 run_analyzer(args, target_dir)
56 number_of_bugs = document(args, target_dir, True)
57 return number_of_bugs if args.status_bugs else 0
58 elif args.intercept_first:
59 # run build command and capture compiler executions
60 exit_code = capture(args, bin_dir)
61 # next step to run the analyzer against the captured commands
62 if need_analyzer(args.build):
63 run_analyzer(args, target_dir)
64 # cover report generation and bug counting
65 number_of_bugs = document(args, target_dir, True)
66 # remove the compilation database when it was not requested
67 if os.path.exists(args.cdb):
68 os.unlink(args.cdb)
69 # set exit status as it was requested
70 return number_of_bugs if args.status_bugs else exit_code
71 else:
72 return exit_code
73 else:
74 # run the build command with compiler wrappers which
75 # execute the analyzer too. (interposition)
76 environment = setup_environment(args, target_dir, bin_dir)
Laszlo Nagy52c1d7e2017-02-14 10:30:50 +000077 exit_code = run_build(args.build, env=environment)
Laszlo Nagybc687582016-01-12 22:38:41 +000078 # cover report generation and bug counting
79 number_of_bugs = document(args, target_dir, False)
80 # set exit status as it was requested
81 return number_of_bugs if args.status_bugs else exit_code
82
83
84def need_analyzer(args):
85 """ Check the intent of the build command.
86
87 When static analyzer run against project configure step, it should be
88 silent and no need to run the analyzer or generate report.
89
90 To run `scan-build` against the configure step might be neccessary,
91 when compiler wrappers are used. That's the moment when build setup
92 check the compiler and capture the location for the build process. """
93
94 return len(args) and not re.search('configure|autogen', args[0])
95
96
97def run_analyzer(args, output_dir):
98 """ Runs the analyzer against the given compilation database. """
99
100 def exclude(filename):
101 """ Return true when any excluded directory prefix the filename. """
102 return any(re.match(r'^' + directory, filename)
103 for directory in args.excludes)
104
105 consts = {
106 'clang': args.clang,
107 'output_dir': output_dir,
108 'output_format': args.output_format,
109 'output_failures': args.output_failures,
Yury Gribova6560eb2016-02-18 11:08:46 +0000110 'direct_args': analyzer_params(args),
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000111 'force_debug': args.force_debug
Laszlo Nagybc687582016-01-12 22:38:41 +0000112 }
113
114 logging.debug('run analyzer against compilation database')
115 with open(args.cdb, 'r') as handle:
116 generator = (dict(cmd, **consts)
117 for cmd in json.load(handle) if not exclude(cmd['file']))
118 # when verbose output requested execute sequentially
119 pool = multiprocessing.Pool(1 if args.verbose > 2 else None)
120 for current in pool.imap_unordered(run, generator):
121 if current is not None:
122 # display error message from the static analyzer
123 for line in current['error_output']:
124 logging.info(line.rstrip())
125 pool.close()
126 pool.join()
127
128
129def setup_environment(args, destination, bin_dir):
130 """ Set up environment for build command to interpose compiler wrapper. """
131
132 environment = dict(os.environ)
133 environment.update({
134 'CC': os.path.join(bin_dir, COMPILER_WRAPPER_CC),
135 'CXX': os.path.join(bin_dir, COMPILER_WRAPPER_CXX),
136 'ANALYZE_BUILD_CC': args.cc,
137 'ANALYZE_BUILD_CXX': args.cxx,
138 'ANALYZE_BUILD_CLANG': args.clang if need_analyzer(args.build) else '',
139 'ANALYZE_BUILD_VERBOSE': 'DEBUG' if args.verbose > 2 else 'WARNING',
140 'ANALYZE_BUILD_REPORT_DIR': destination,
141 'ANALYZE_BUILD_REPORT_FORMAT': args.output_format,
142 'ANALYZE_BUILD_REPORT_FAILURES': 'yes' if args.output_failures else '',
Yury Gribova6560eb2016-02-18 11:08:46 +0000143 'ANALYZE_BUILD_PARAMETERS': ' '.join(analyzer_params(args)),
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000144 'ANALYZE_BUILD_FORCE_DEBUG': 'yes' if args.force_debug else ''
Laszlo Nagybc687582016-01-12 22:38:41 +0000145 })
146 return environment
147
148
149def analyze_build_wrapper(cplusplus):
150 """ Entry point for `analyze-cc` and `analyze-c++` compiler wrappers. """
151
152 # initialize wrapper logging
153 logging.basicConfig(format='analyze: %(levelname)s: %(message)s',
154 level=os.getenv('ANALYZE_BUILD_VERBOSE', 'INFO'))
155 # execute with real compiler
156 compiler = os.getenv('ANALYZE_BUILD_CXX', 'c++') if cplusplus \
157 else os.getenv('ANALYZE_BUILD_CC', 'cc')
158 compilation = [compiler] + sys.argv[1:]
159 logging.info('execute compiler: %s', compilation)
160 result = subprocess.call(compilation)
161 # exit when it fails, ...
162 if result or not os.getenv('ANALYZE_BUILD_CLANG'):
163 return result
164 # ... and run the analyzer if all went well.
165 try:
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000166 # check is it a compilation
167 compilation = split_command(sys.argv)
168 if compilation is None:
169 return result
Laszlo Nagybc687582016-01-12 22:38:41 +0000170 # collect the needed parameters from environment, crash when missing
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000171 parameters = {
Laszlo Nagybc687582016-01-12 22:38:41 +0000172 'clang': os.getenv('ANALYZE_BUILD_CLANG'),
173 'output_dir': os.getenv('ANALYZE_BUILD_REPORT_DIR'),
174 'output_format': os.getenv('ANALYZE_BUILD_REPORT_FORMAT'),
175 'output_failures': os.getenv('ANALYZE_BUILD_REPORT_FAILURES'),
176 'direct_args': os.getenv('ANALYZE_BUILD_PARAMETERS',
177 '').split(' '),
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000178 'force_debug': os.getenv('ANALYZE_BUILD_FORCE_DEBUG'),
Laszlo Nagybc687582016-01-12 22:38:41 +0000179 'directory': os.getcwd(),
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000180 'command': [sys.argv[0], '-c'] + compilation.flags
Laszlo Nagybc687582016-01-12 22:38:41 +0000181 }
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000182 # call static analyzer against the compilation
183 for source in compilation.files:
184 parameters.update({'file': source})
Laszlo Nagybc687582016-01-12 22:38:41 +0000185 logging.debug('analyzer parameters %s', parameters)
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000186 current = run(parameters)
Laszlo Nagybc687582016-01-12 22:38:41 +0000187 # display error message from the static analyzer
188 if current is not None:
189 for line in current['error_output']:
190 logging.info(line.rstrip())
191 except Exception:
192 logging.exception("run analyzer inside compiler wrapper failed.")
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000193 return result
Laszlo Nagybc687582016-01-12 22:38:41 +0000194
195
Laszlo Nagy258ff252017-02-14 10:43:38 +0000196@contextlib.contextmanager
197def report_directory(hint, keep):
198 """ Responsible for the report directory.
199
200 hint -- could specify the parent directory of the output directory.
201 keep -- a boolean value to keep or delete the empty report directory. """
202
203 stamp_format = 'scan-build-%Y-%m-%d-%H-%M-%S-%f-'
204 stamp = datetime.datetime.now().strftime(stamp_format)
205 parent_dir = os.path.abspath(hint)
206 if not os.path.exists(parent_dir):
207 os.makedirs(parent_dir)
208 name = tempfile.mkdtemp(prefix=stamp, dir=parent_dir)
209
210 logging.info('Report directory created: %s', name)
211
212 try:
213 yield name
214 finally:
215 if os.listdir(name):
216 msg = "Run 'scan-view %s' to examine bug reports."
217 keep = True
218 else:
219 if keep:
220 msg = "Report directory '%s' contains no report, but kept."
221 else:
222 msg = "Removing directory '%s' because it contains no report."
223 logging.warning(msg, name)
224
225 if not keep:
226 os.rmdir(name)
227
228
Laszlo Nagybc687582016-01-12 22:38:41 +0000229def analyzer_params(args):
230 """ A group of command line arguments can mapped to command
231 line arguments of the analyzer. This method generates those. """
232
233 def prefix_with(constant, pieces):
234 """ From a sequence create another sequence where every second element
235 is from the original sequence and the odd elements are the prefix.
236
237 eg.: prefix_with(0, [1,2,3]) creates [0, 1, 0, 2, 0, 3] """
238
239 return [elem for piece in pieces for elem in [constant, piece]]
240
241 result = []
242
243 if args.store_model:
244 result.append('-analyzer-store={0}'.format(args.store_model))
245 if args.constraints_model:
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000246 result.append('-analyzer-constraints={0}'.format(
247 args.constraints_model))
Laszlo Nagybc687582016-01-12 22:38:41 +0000248 if args.internal_stats:
249 result.append('-analyzer-stats')
250 if args.analyze_headers:
251 result.append('-analyzer-opt-analyze-headers')
252 if args.stats:
253 result.append('-analyzer-checker=debug.Stats')
254 if args.maxloop:
255 result.extend(['-analyzer-max-loop', str(args.maxloop)])
256 if args.output_format:
257 result.append('-analyzer-output={0}'.format(args.output_format))
258 if args.analyzer_config:
259 result.append(args.analyzer_config)
260 if args.verbose >= 4:
261 result.append('-analyzer-display-progress')
262 if args.plugins:
263 result.extend(prefix_with('-load', args.plugins))
264 if args.enable_checker:
265 checkers = ','.join(args.enable_checker)
266 result.extend(['-analyzer-checker', checkers])
267 if args.disable_checker:
268 checkers = ','.join(args.disable_checker)
269 result.extend(['-analyzer-disable-checker', checkers])
270 if os.getenv('UBIVIZ'):
271 result.append('-analyzer-viz-egraph-ubigraph')
272
273 return prefix_with('-Xclang', result)
274
275
276def print_active_checkers(checkers):
277 """ Print active checkers to stdout. """
278
279 for name in sorted(name for name, (_, active) in checkers.items()
280 if active):
281 print(name)
282
283
284def print_checkers(checkers):
285 """ Print verbose checker help to stdout. """
286
287 print('')
288 print('available checkers:')
289 print('')
290 for name in sorted(checkers.keys()):
291 description, active = checkers[name]
292 prefix = '+' if active else ' '
293 if len(name) > 30:
294 print(' {0} {1}'.format(prefix, name))
295 print(' ' * 35 + description)
296 else:
297 print(' {0} {1: <30} {2}'.format(prefix, name, description))
298 print('')
299 print('NOTE: "+" indicates that an analysis is enabled by default.')
300 print('')
301
302
303def validate(parser, args, from_build_command):
304 """ Validation done by the parser itself, but semantic check still
305 needs to be done. This method is doing that. """
306
Laszlo Nagy4f6a1752016-09-24 00:20:59 +0000307 # Make plugins always a list. (It might be None when not specified.)
308 args.plugins = args.plugins if args.plugins else []
309
Laszlo Nagybc687582016-01-12 22:38:41 +0000310 if args.help_checkers_verbose:
311 print_checkers(get_checkers(args.clang, args.plugins))
312 parser.exit()
313 elif args.help_checkers:
314 print_active_checkers(get_checkers(args.clang, args.plugins))
315 parser.exit()
316
317 if from_build_command and not args.build:
318 parser.error('missing build command')
319
320
321def create_parser(from_build_command):
322 """ Command line argument parser factory method. """
323
324 parser = argparse.ArgumentParser(
325 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
326
327 parser.add_argument(
328 '--verbose', '-v',
329 action='count',
330 default=0,
331 help="""Enable verbose output from '%(prog)s'. A second and third
332 flag increases verbosity.""")
333 parser.add_argument(
334 '--override-compiler',
335 action='store_true',
336 help="""Always resort to the compiler wrapper even when better
337 interposition methods are available.""")
338 parser.add_argument(
339 '--intercept-first',
340 action='store_true',
341 help="""Run the build commands only, build a compilation database,
342 then run the static analyzer afterwards.
343 Generally speaking it has better coverage on build commands.
344 With '--override-compiler' it use compiler wrapper, but does
345 not run the analyzer till the build is finished. """)
346 parser.add_argument(
347 '--cdb',
348 metavar='<file>',
349 default="compile_commands.json",
350 help="""The JSON compilation database.""")
351
352 parser.add_argument(
353 '--output', '-o',
354 metavar='<path>',
355 default=tempdir(),
356 help="""Specifies the output directory for analyzer reports.
357 Subdirectory will be created if default directory is targeted.
358 """)
359 parser.add_argument(
360 '--status-bugs',
361 action='store_true',
362 help="""By default, the exit status of '%(prog)s' is the same as the
363 executed build command. Specifying this option causes the exit
364 status of '%(prog)s' to be non zero if it found potential bugs
365 and zero otherwise.""")
366 parser.add_argument(
367 '--html-title',
368 metavar='<title>',
369 help="""Specify the title used on generated HTML pages.
370 If not specified, a default title will be used.""")
371 parser.add_argument(
372 '--analyze-headers',
373 action='store_true',
374 help="""Also analyze functions in #included files. By default, such
375 functions are skipped unless they are called by functions
376 within the main source file.""")
377 format_group = parser.add_mutually_exclusive_group()
378 format_group.add_argument(
379 '--plist', '-plist',
380 dest='output_format',
381 const='plist',
382 default='html',
383 action='store_const',
384 help="""This option outputs the results as a set of .plist files.""")
385 format_group.add_argument(
386 '--plist-html', '-plist-html',
387 dest='output_format',
388 const='plist-html',
389 default='html',
390 action='store_const',
391 help="""This option outputs the results as a set of .html and .plist
392 files.""")
393 # TODO: implement '-view '
394
395 advanced = parser.add_argument_group('advanced options')
396 advanced.add_argument(
397 '--keep-empty',
398 action='store_true',
399 help="""Don't remove the build results directory even if no issues
400 were reported.""")
401 advanced.add_argument(
402 '--no-failure-reports', '-no-failure-reports',
403 dest='output_failures',
404 action='store_false',
405 help="""Do not create a 'failures' subdirectory that includes analyzer
406 crash reports and preprocessed source files.""")
407 advanced.add_argument(
408 '--stats', '-stats',
409 action='store_true',
410 help="""Generates visitation statistics for the project being analyzed.
411 """)
412 advanced.add_argument(
413 '--internal-stats',
414 action='store_true',
415 help="""Generate internal analyzer statistics.""")
416 advanced.add_argument(
417 '--maxloop', '-maxloop',
418 metavar='<loop count>',
419 type=int,
420 help="""Specifiy the number of times a block can be visited before
421 giving up. Increase for more comprehensive coverage at a cost
422 of speed.""")
423 advanced.add_argument(
424 '--store', '-store',
425 metavar='<model>',
426 dest='store_model',
427 choices=['region', 'basic'],
428 help="""Specify the store model used by the analyzer.
429 'region' specifies a field- sensitive store model.
430 'basic' which is far less precise but can more quickly
431 analyze code. 'basic' was the default store model for
432 checker-0.221 and earlier.""")
433 advanced.add_argument(
434 '--constraints', '-constraints',
435 metavar='<model>',
436 dest='constraints_model',
437 choices=['range', 'basic'],
438 help="""Specify the contraint engine used by the analyzer. Specifying
439 'basic' uses a simpler, less powerful constraint model used by
440 checker-0.160 and earlier.""")
441 advanced.add_argument(
442 '--use-analyzer',
443 metavar='<path>',
444 dest='clang',
445 default='clang',
446 help="""'%(prog)s' uses the 'clang' executable relative to itself for
447 static analysis. One can override this behavior with this
448 option by using the 'clang' packaged with Xcode (on OS X) or
449 from the PATH.""")
450 advanced.add_argument(
451 '--use-cc',
452 metavar='<path>',
453 dest='cc',
454 default='cc',
455 help="""When '%(prog)s' analyzes a project by interposing a "fake
456 compiler", which executes a real compiler for compilation and
457 do other tasks (to run the static analyzer or just record the
458 compiler invocation). Because of this interposing, '%(prog)s'
459 does not know what compiler your project normally uses.
460 Instead, it simply overrides the CC environment variable, and
461 guesses your default compiler.
462
463 If you need '%(prog)s' to use a specific compiler for
464 *compilation* then you can use this option to specify a path
465 to that compiler.""")
466 advanced.add_argument(
467 '--use-c++',
468 metavar='<path>',
469 dest='cxx',
470 default='c++',
471 help="""This is the same as "--use-cc" but for C++ code.""")
472 advanced.add_argument(
473 '--analyzer-config', '-analyzer-config',
474 metavar='<options>',
475 help="""Provide options to pass through to the analyzer's
476 -analyzer-config flag. Several options are separated with
477 comma: 'key1=val1,key2=val2'
478
479 Available options:
480 stable-report-filename=true or false (default)
481
482 Switch the page naming to:
483 report-<filename>-<function/method name>-<id>.html
484 instead of report-XXXXXX.html""")
485 advanced.add_argument(
486 '--exclude',
487 metavar='<directory>',
488 dest='excludes',
489 action='append',
490 default=[],
491 help="""Do not run static analyzer against files found in this
492 directory. (You can specify this option multiple times.)
493 Could be usefull when project contains 3rd party libraries.
494 The directory path shall be absolute path as file names in
495 the compilation database.""")
Yury Gribova6560eb2016-02-18 11:08:46 +0000496 advanced.add_argument(
497 '--force-analyze-debug-code',
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000498 dest='force_debug',
Yury Gribova6560eb2016-02-18 11:08:46 +0000499 action='store_true',
500 help="""Tells analyzer to enable assertions in code even if they were
Laszlo Nagy8bd63e52016-04-19 12:03:03 +0000501 disabled during compilation, enabling more precise results.""")
Laszlo Nagybc687582016-01-12 22:38:41 +0000502
503 plugins = parser.add_argument_group('checker options')
504 plugins.add_argument(
505 '--load-plugin', '-load-plugin',
506 metavar='<plugin library>',
507 dest='plugins',
508 action='append',
509 help="""Loading external checkers using the clang plugin interface.""")
510 plugins.add_argument(
511 '--enable-checker', '-enable-checker',
512 metavar='<checker name>',
513 action=AppendCommaSeparated,
514 help="""Enable specific checker.""")
515 plugins.add_argument(
516 '--disable-checker', '-disable-checker',
517 metavar='<checker name>',
518 action=AppendCommaSeparated,
519 help="""Disable specific checker.""")
520 plugins.add_argument(
521 '--help-checkers',
522 action='store_true',
523 help="""A default group of checkers is run unless explicitly disabled.
524 Exactly which checkers constitute the default group is a
525 function of the operating system in use. These can be printed
526 with this flag.""")
527 plugins.add_argument(
528 '--help-checkers-verbose',
529 action='store_true',
530 help="""Print all available checkers and mark the enabled ones.""")
531
532 if from_build_command:
533 parser.add_argument(
534 dest='build',
535 nargs=argparse.REMAINDER,
536 help="""Command to run.""")
537
538 return parser
539
540
541class AppendCommaSeparated(argparse.Action):
542 """ argparse Action class to support multiple comma separated lists. """
543
544 def __call__(self, __parser, namespace, values, __option_string):
545 # getattr(obj, attr, default) does not really returns default but none
546 if getattr(namespace, self.dest, None) is None:
547 setattr(namespace, self.dest, [])
548 # once it's fixed we can use as expected
549 actual = getattr(namespace, self.dest)
550 actual.extend(values.split(','))
551 setattr(namespace, self.dest, actual)