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