1 | # -*- coding: utf-8 -*- |
2 | # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
3 | # See https://llvm.org/LICENSE.txt for license information. |
4 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
5 | """ This module parses and validates arguments for command-line interfaces. |
6 | |
7 | It uses argparse module to create the command line parser. (This library is |
8 | in the standard python library since 3.2 and backported to 2.7, but not |
9 | earlier.) |
10 | |
11 | It also implements basic validation methods, related to the command. |
12 | Validations are mostly calling specific help methods, or mangling values. |
13 | """ |
14 | from __future__ import absolute_import, division, print_function |
15 | |
16 | import os |
17 | import sys |
18 | import argparse |
19 | import logging |
20 | import tempfile |
21 | from libscanbuild import reconfigure_logging, CtuConfig |
22 | from libscanbuild.clang import get_checkers, is_ctu_capable |
23 | |
24 | __all__ = ['parse_args_for_intercept_build', 'parse_args_for_analyze_build', |
25 | 'parse_args_for_scan_build'] |
26 | |
27 | |
28 | def parse_args_for_intercept_build(): |
29 | """ Parse and validate command-line arguments for intercept-build. """ |
30 | |
31 | parser = create_intercept_parser() |
32 | args = parser.parse_args() |
33 | |
34 | reconfigure_logging(args.verbose) |
35 | logging.debug('Raw arguments %s', sys.argv) |
36 | |
37 | # short validation logic |
38 | if not args.build: |
39 | parser.error(message='missing build command') |
40 | |
41 | logging.debug('Parsed arguments: %s', args) |
42 | return args |
43 | |
44 | |
45 | def parse_args_for_analyze_build(): |
46 | """ Parse and validate command-line arguments for analyze-build. """ |
47 | |
48 | from_build_command = False |
49 | parser = create_analyze_parser(from_build_command) |
50 | args = parser.parse_args() |
51 | |
52 | reconfigure_logging(args.verbose) |
53 | logging.debug('Raw arguments %s', sys.argv) |
54 | |
55 | normalize_args_for_analyze(args, from_build_command) |
56 | validate_args_for_analyze(parser, args, from_build_command) |
57 | logging.debug('Parsed arguments: %s', args) |
58 | return args |
59 | |
60 | |
61 | def parse_args_for_scan_build(): |
62 | """ Parse and validate command-line arguments for scan-build. """ |
63 | |
64 | from_build_command = True |
65 | parser = create_analyze_parser(from_build_command) |
66 | args = parser.parse_args() |
67 | |
68 | reconfigure_logging(args.verbose) |
69 | logging.debug('Raw arguments %s', sys.argv) |
70 | |
71 | normalize_args_for_analyze(args, from_build_command) |
72 | validate_args_for_analyze(parser, args, from_build_command) |
73 | logging.debug('Parsed arguments: %s', args) |
74 | return args |
75 | |
76 | |
77 | def normalize_args_for_analyze(args, from_build_command): |
78 | """ Normalize parsed arguments for analyze-build and scan-build. |
79 | |
80 | :param args: Parsed argument object. (Will be mutated.) |
81 | :param from_build_command: Boolean value tells is the command suppose |
82 | to run the analyzer against a build command or a compilation db. """ |
83 | |
84 | # make plugins always a list. (it might be None when not specified.) |
85 | if args.plugins is None: |
86 | args.plugins = [] |
87 | |
88 | # make exclude directory list unique and absolute. |
89 | uniq_excludes = set(os.path.abspath(entry) for entry in args.excludes) |
90 | args.excludes = list(uniq_excludes) |
91 | |
92 | # because shared codes for all tools, some common used methods are |
93 | # expecting some argument to be present. so, instead of query the args |
94 | # object about the presence of the flag, we fake it here. to make those |
95 | # methods more readable. (it's an arguable choice, took it only for those |
96 | # which have good default value.) |
97 | if from_build_command: |
98 | # add cdb parameter invisibly to make report module working. |
99 | args.cdb = 'compile_commands.json' |
100 | |
101 | # Make ctu_dir an abspath as it is needed inside clang |
102 | if not from_build_command and hasattr(args, 'ctu_phases') \ |
103 | and hasattr(args.ctu_phases, 'dir'): |
104 | args.ctu_dir = os.path.abspath(args.ctu_dir) |
105 | |
106 | |
107 | def validate_args_for_analyze(parser, args, from_build_command): |
108 | """ Command line parsing is done by the argparse module, but semantic |
109 | validation still needs to be done. This method is doing it for |
110 | analyze-build and scan-build commands. |
111 | |
112 | :param parser: The command line parser object. |
113 | :param args: Parsed argument object. |
114 | :param from_build_command: Boolean value tells is the command suppose |
115 | to run the analyzer against a build command or a compilation db. |
116 | :return: No return value, but this call might throw when validation |
117 | fails. """ |
118 | |
119 | if args.help_checkers_verbose: |
120 | print_checkers(get_checkers(args.clang, args.plugins)) |
121 | parser.exit(status=0) |
122 | elif args.help_checkers: |
123 | print_active_checkers(get_checkers(args.clang, args.plugins)) |
124 | parser.exit(status=0) |
125 | elif from_build_command and not args.build: |
126 | parser.error(message='missing build command') |
127 | elif not from_build_command and not os.path.exists(args.cdb): |
128 | parser.error(message='compilation database is missing') |
129 | |
130 | # If the user wants CTU mode |
131 | if not from_build_command and hasattr(args, 'ctu_phases') \ |
132 | and hasattr(args.ctu_phases, 'dir'): |
133 | # If CTU analyze_only, the input directory should exist |
134 | if args.ctu_phases.analyze and not args.ctu_phases.collect \ |
135 | and not os.path.exists(args.ctu_dir): |
136 | parser.error(message='missing CTU directory') |
137 | # Check CTU capability via checking clang-extdef-mapping |
138 | if not is_ctu_capable(args.extdef_map_cmd): |
139 | parser.error(message="""This version of clang does not support CTU |
140 | functionality or clang-extdef-mapping command not found.""") |
141 | |
142 | |
143 | def create_intercept_parser(): |
144 | """ Creates a parser for command-line arguments to 'intercept'. """ |
145 | |
146 | parser = create_default_parser() |
147 | parser_add_cdb(parser) |
148 | |
149 | parser_add_prefer_wrapper(parser) |
150 | parser_add_compilers(parser) |
151 | |
152 | advanced = parser.add_argument_group('advanced options') |
153 | group = advanced.add_mutually_exclusive_group() |
154 | group.add_argument( |
155 | '--append', |
156 | action='store_true', |
157 | help="""Extend existing compilation database with new entries. |
158 | Duplicate entries are detected and not present in the final output. |
159 | The output is not continuously updated, it's done when the build |
160 | command finished. """) |
161 | |
162 | parser.add_argument( |
163 | dest='build', nargs=argparse.REMAINDER, help="""Command to run.""") |
164 | return parser |
165 | |
166 | |
167 | def create_analyze_parser(from_build_command): |
168 | """ Creates a parser for command-line arguments to 'analyze'. """ |
169 | |
170 | parser = create_default_parser() |
171 | |
172 | if from_build_command: |
173 | parser_add_prefer_wrapper(parser) |
174 | parser_add_compilers(parser) |
175 | |
176 | parser.add_argument( |
177 | '--intercept-first', |
178 | action='store_true', |
179 | help="""Run the build commands first, intercept compiler |
180 | calls and then run the static analyzer afterwards. |
181 | Generally speaking it has better coverage on build commands. |
182 | With '--override-compiler' it use compiler wrapper, but does |
183 | not run the analyzer till the build is finished.""") |
184 | else: |
185 | parser_add_cdb(parser) |
186 | |
187 | parser.add_argument( |
188 | '--status-bugs', |
189 | action='store_true', |
190 | help="""The exit status of '%(prog)s' is the same as the executed |
191 | build command. This option ignores the build exit status and sets to |
192 | be non zero if it found potential bugs or zero otherwise.""") |
193 | parser.add_argument( |
194 | '--exclude', |
195 | metavar='<directory>', |
196 | dest='excludes', |
197 | action='append', |
198 | default=[], |
199 | help="""Do not run static analyzer against files found in this |
200 | directory. (You can specify this option multiple times.) |
201 | Could be useful when project contains 3rd party libraries.""") |
202 | |
203 | output = parser.add_argument_group('output control options') |
204 | output.add_argument( |
205 | '--output', |
206 | '-o', |
207 | metavar='<path>', |
208 | default=tempfile.gettempdir(), |
209 | help="""Specifies the output directory for analyzer reports. |
210 | Subdirectory will be created if default directory is targeted.""") |
211 | output.add_argument( |
212 | '--keep-empty', |
213 | action='store_true', |
214 | help="""Don't remove the build results directory even if no issues |
215 | were reported.""") |
216 | output.add_argument( |
217 | '--html-title', |
218 | metavar='<title>', |
219 | help="""Specify the title used on generated HTML pages. |
220 | If not specified, a default title will be used.""") |
221 | format_group = output.add_mutually_exclusive_group() |
222 | format_group.add_argument( |
223 | '--plist', |
224 | '-plist', |
225 | dest='output_format', |
226 | const='plist', |
227 | default='html', |
228 | action='store_const', |
229 | help="""Cause the results as a set of .plist files.""") |
230 | format_group.add_argument( |
231 | '--plist-html', |
232 | '-plist-html', |
233 | dest='output_format', |
234 | const='plist-html', |
235 | default='html', |
236 | action='store_const', |
237 | help="""Cause the results as a set of .html and .plist files.""") |
238 | format_group.add_argument( |
239 | '--plist-multi-file', |
240 | '-plist-multi-file', |
241 | dest='output_format', |
242 | const='plist-multi-file', |
243 | default='html', |
244 | action='store_const', |
245 | help="""Cause the results as a set of .plist files with extra |
246 | information on related files.""") |
247 | |
248 | advanced = parser.add_argument_group('advanced options') |
249 | advanced.add_argument( |
250 | '--use-analyzer', |
251 | metavar='<path>', |
252 | dest='clang', |
253 | default='clang', |
254 | help="""'%(prog)s' uses the 'clang' executable relative to itself for |
255 | static analysis. One can override this behavior with this option by |
256 | using the 'clang' packaged with Xcode (on OS X) or from the PATH.""") |
257 | advanced.add_argument( |
258 | '--no-failure-reports', |
259 | '-no-failure-reports', |
260 | dest='output_failures', |
261 | action='store_false', |
262 | help="""Do not create a 'failures' subdirectory that includes analyzer |
263 | crash reports and preprocessed source files.""") |
264 | parser.add_argument( |
265 | '--analyze-headers', |
266 | action='store_true', |
267 | help="""Also analyze functions in #included files. By default, such |
268 | functions are skipped unless they are called by functions within the |
269 | main source file.""") |
270 | advanced.add_argument( |
271 | '--stats', |
272 | '-stats', |
273 | action='store_true', |
274 | help="""Generates visitation statistics for the project.""") |
275 | advanced.add_argument( |
276 | '--internal-stats', |
277 | action='store_true', |
278 | help="""Generate internal analyzer statistics.""") |
279 | advanced.add_argument( |
280 | '--maxloop', |
281 | '-maxloop', |
282 | metavar='<loop count>', |
283 | type=int, |
284 | help="""Specify the number of times a block can be visited before |
285 | giving up. Increase for more comprehensive coverage at a cost of |
286 | speed.""") |
287 | advanced.add_argument( |
288 | '--store', |
289 | '-store', |
290 | metavar='<model>', |
291 | dest='store_model', |
292 | choices=['region', 'basic'], |
293 | help="""Specify the store model used by the analyzer. 'region' |
294 | specifies a field- sensitive store model. 'basic' which is far less |
295 | precise but can more quickly analyze code. 'basic' was the default |
296 | store model for checker-0.221 and earlier.""") |
297 | advanced.add_argument( |
298 | '--constraints', |
299 | '-constraints', |
300 | metavar='<model>', |
301 | dest='constraints_model', |
302 | choices=['range', 'basic'], |
303 | help="""Specify the constraint engine used by the analyzer. Specifying |
304 | 'basic' uses a simpler, less powerful constraint model used by |
305 | checker-0.160 and earlier.""") |
306 | advanced.add_argument( |
307 | '--analyzer-config', |
308 | '-analyzer-config', |
309 | metavar='<options>', |
310 | help="""Provide options to pass through to the analyzer's |
311 | -analyzer-config flag. Several options are separated with comma: |
312 | 'key1=val1,key2=val2' |
313 | |
314 | Available options: |
315 | stable-report-filename=true or false (default) |
316 | |
317 | Switch the page naming to: |
318 | report-<filename>-<function/method name>-<id>.html |
319 | instead of report-XXXXXX.html""") |
320 | advanced.add_argument( |
321 | '--force-analyze-debug-code', |
322 | dest='force_debug', |
323 | action='store_true', |
324 | help="""Tells analyzer to enable assertions in code even if they were |
325 | disabled during compilation, enabling more precise results.""") |
326 | |
327 | plugins = parser.add_argument_group('checker options') |
328 | plugins.add_argument( |
329 | '--load-plugin', |
330 | '-load-plugin', |
331 | metavar='<plugin library>', |
332 | dest='plugins', |
333 | action='append', |
334 | help="""Loading external checkers using the clang plugin interface.""") |
335 | plugins.add_argument( |
336 | '--enable-checker', |
337 | '-enable-checker', |
338 | metavar='<checker name>', |
339 | action=AppendCommaSeparated, |
340 | help="""Enable specific checker.""") |
341 | plugins.add_argument( |
342 | '--disable-checker', |
343 | '-disable-checker', |
344 | metavar='<checker name>', |
345 | action=AppendCommaSeparated, |
346 | help="""Disable specific checker.""") |
347 | plugins.add_argument( |
348 | '--help-checkers', |
349 | action='store_true', |
350 | help="""A default group of checkers is run unless explicitly disabled. |
351 | Exactly which checkers constitute the default group is a function of |
352 | the operating system in use. These can be printed with this flag.""") |
353 | plugins.add_argument( |
354 | '--help-checkers-verbose', |
355 | action='store_true', |
356 | help="""Print all available checkers and mark the enabled ones.""") |
357 | |
358 | if from_build_command: |
359 | parser.add_argument( |
360 | dest='build', nargs=argparse.REMAINDER, help="""Command to run.""") |
361 | else: |
362 | ctu = parser.add_argument_group('cross translation unit analysis') |
363 | ctu_mutex_group = ctu.add_mutually_exclusive_group() |
364 | ctu_mutex_group.add_argument( |
365 | '--ctu', |
366 | action='store_const', |
367 | const=CtuConfig(collect=True, analyze=True, |
368 | dir='', extdef_map_cmd=''), |
369 | dest='ctu_phases', |
370 | help="""Perform cross translation unit (ctu) analysis (both collect |
371 | and analyze phases) using default <ctu-dir> for temporary output. |
372 | At the end of the analysis, the temporary directory is removed.""") |
373 | ctu.add_argument( |
374 | '--ctu-dir', |
375 | metavar='<ctu-dir>', |
376 | dest='ctu_dir', |
377 | default='ctu-dir', |
378 | help="""Defines the temporary directory used between ctu |
379 | phases.""") |
380 | ctu_mutex_group.add_argument( |
381 | '--ctu-collect-only', |
382 | action='store_const', |
383 | const=CtuConfig(collect=True, analyze=False, |
384 | dir='', extdef_map_cmd=''), |
385 | dest='ctu_phases', |
386 | help="""Perform only the collect phase of ctu. |
387 | Keep <ctu-dir> for further use.""") |
388 | ctu_mutex_group.add_argument( |
389 | '--ctu-analyze-only', |
390 | action='store_const', |
391 | const=CtuConfig(collect=False, analyze=True, |
392 | dir='', extdef_map_cmd=''), |
393 | dest='ctu_phases', |
394 | help="""Perform only the analyze phase of ctu. <ctu-dir> should be |
395 | present and will not be removed after analysis.""") |
396 | ctu.add_argument( |
397 | '--use-extdef-map-cmd', |
398 | metavar='<path>', |
399 | dest='extdef_map_cmd', |
400 | default='clang-extdef-mapping', |
401 | help="""'%(prog)s' uses the 'clang-extdef-mapping' executable |
402 | relative to itself for generating external definition maps for |
403 | static analysis. One can override this behavior with this option |
404 | by using the 'clang-extdef-mapping' packaged with Xcode (on OS X) |
405 | or from the PATH.""") |
406 | return parser |
407 | |
408 | |
409 | def create_default_parser(): |
410 | """ Creates command line parser for all build wrapper commands. """ |
411 | |
412 | parser = argparse.ArgumentParser( |
413 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
414 | |
415 | parser.add_argument( |
416 | '--verbose', |
417 | '-v', |
418 | action='count', |
419 | default=0, |
420 | help="""Enable verbose output from '%(prog)s'. A second, third and |
421 | fourth flags increases verbosity.""") |
422 | return parser |
423 | |
424 | |
425 | def parser_add_cdb(parser): |
426 | parser.add_argument( |
427 | '--cdb', |
428 | metavar='<file>', |
429 | default="compile_commands.json", |
430 | help="""The JSON compilation database.""") |
431 | |
432 | |
433 | def parser_add_prefer_wrapper(parser): |
434 | parser.add_argument( |
435 | '--override-compiler', |
436 | action='store_true', |
437 | help="""Always resort to the compiler wrapper even when better |
438 | intercept methods are available.""") |
439 | |
440 | |
441 | def parser_add_compilers(parser): |
442 | parser.add_argument( |
443 | '--use-cc', |
444 | metavar='<path>', |
445 | dest='cc', |
446 | default=os.getenv('CC', 'cc'), |
447 | help="""When '%(prog)s' analyzes a project by interposing a compiler |
448 | wrapper, which executes a real compiler for compilation and do other |
449 | tasks (record the compiler invocation). Because of this interposing, |
450 | '%(prog)s' does not know what compiler your project normally uses. |
451 | Instead, it simply overrides the CC environment variable, and guesses |
452 | your default compiler. |
453 | |
454 | If you need '%(prog)s' to use a specific compiler for *compilation* |
455 | then you can use this option to specify a path to that compiler.""") |
456 | parser.add_argument( |
457 | '--use-c++', |
458 | metavar='<path>', |
459 | dest='cxx', |
460 | default=os.getenv('CXX', 'c++'), |
461 | help="""This is the same as "--use-cc" but for C++ code.""") |
462 | |
463 | |
464 | class AppendCommaSeparated(argparse.Action): |
465 | """ argparse Action class to support multiple comma separated lists. """ |
466 | |
467 | def __call__(self, __parser, namespace, values, __option_string): |
468 | # getattr(obj, attr, default) does not really returns default but none |
469 | if getattr(namespace, self.dest, None) is None: |
470 | setattr(namespace, self.dest, []) |
471 | # once it's fixed we can use as expected |
472 | actual = getattr(namespace, self.dest) |
473 | actual.extend(values.split(',')) |
474 | setattr(namespace, self.dest, actual) |
475 | |
476 | |
477 | def print_active_checkers(checkers): |
478 | """ Print active checkers to stdout. """ |
479 | |
480 | for name in sorted(name for name, (_, active) in checkers.items() |
481 | if active): |
482 | print(name) |
483 | |
484 | |
485 | def print_checkers(checkers): |
486 | """ Print verbose checker help to stdout. """ |
487 | |
488 | print('') |
489 | print('available checkers:') |
490 | print('') |
491 | for name in sorted(checkers.keys()): |
492 | description, active = checkers[name] |
493 | prefix = '+' if active else ' ' |
494 | if len(name) > 30: |
495 | print(' {0} {1}'.format(prefix, name)) |
496 | print(' ' * 35 + description) |
497 | else: |
498 | print(' {0} {1: <30} {2}'.format(prefix, name, description)) |
499 | print('') |
500 | print('NOTE: "+" indicates that an analysis is enabled by default.') |
501 | print('') |
502 | |