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
7It uses argparse module to create the command line parser. (This library is
8in the standard python library since 3.2 and backported to 2.7, but not
9earlier.)
10
11It also implements basic validation methods, related to the command.
12Validations are mostly calling specific help methods, or mangling values.
13"""
14from __future__ import absolute_import, division, print_function
15
16import os
17import sys
18import argparse
19import logging
20import tempfile
21from libscanbuild import reconfigure_logging, CtuConfig
22from 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
28def 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
45def 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
61def 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
77def 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
107def 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
143def 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
167def 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
409def 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
425def 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
433def 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
441def 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
464class 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
477def 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
485def 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