You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							391 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							391 lines
						
					
					
						
							12 KiB
						
					
					
				#!/usr/bin/env python2.7
 | 
						|
 | 
						|
# Copyright (c) Facebook, Inc. and its affiliates.
 | 
						|
#
 | 
						|
# This source code is licensed under the MIT license found in the
 | 
						|
# LICENSE file in the root directory of this source tree.
 | 
						|
 | 
						|
from __future__ import absolute_import
 | 
						|
from __future__ import division
 | 
						|
from __future__ import print_function
 | 
						|
from __future__ import unicode_literals
 | 
						|
 | 
						|
import argparse
 | 
						|
import codecs
 | 
						|
import json
 | 
						|
import os
 | 
						|
import re
 | 
						|
import shutil
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
 | 
						|
from inferlib import colorize, config, issues, source, utils
 | 
						|
 | 
						|
HTML_REPORT_DIR = 'report.html'
 | 
						|
TRACES_REPORT_DIR = 'traces'
 | 
						|
SOURCE_REMOTE_GITHUB_URL_TEMPLATE = ('https://github.com/{project}/blob/'
 | 
						|
                                     '{hash}/{relative-path}/'
 | 
						|
                                     '{file-name}#L{line-number}')
 | 
						|
SOURCE_REMOTE_GITHUB_RE = re.compile('.*github.com[:/](?P<project>.*)')
 | 
						|
 | 
						|
 | 
						|
base_parser = argparse.ArgumentParser(
 | 
						|
    description='Explore the error traces in Infer reports.')
 | 
						|
base_parser.add_argument('-o', '--out', metavar='<directory>',
 | 
						|
                         default=config.DEFAULT_INFER_OUT, dest='infer_out',
 | 
						|
                         action=utils.AbsolutePathAction,
 | 
						|
                         help='Set the Infer results directory')
 | 
						|
base_parser.add_argument('--only-show',
 | 
						|
                         action='store_true',
 | 
						|
                         help='Show the list of reports and exit')
 | 
						|
base_parser.add_argument('--no-source',
 | 
						|
                         action='store_true',
 | 
						|
                         help='Do not print code excerpts')
 | 
						|
base_parser.add_argument('--select',
 | 
						|
                         metavar='N',
 | 
						|
                         type=int,
 | 
						|
                         help='Select bug number N. '
 | 
						|
                         'If omitted, prompts you for input.')
 | 
						|
base_parser.add_argument('--max-level',
 | 
						|
                         metavar='N',
 | 
						|
                         type=int,
 | 
						|
                         help='Level of nested procedure calls to show. '
 | 
						|
                         'By default, all levels are shown.')
 | 
						|
base_parser.add_argument('--html',
 | 
						|
                         action='store_true',
 | 
						|
                         help='Generate HTML report.')
 | 
						|
 | 
						|
 | 
						|
def show_error_and_exit(err, show_help):
 | 
						|
    utils.stderr(err)
 | 
						|
    if show_help:
 | 
						|
        print('')
 | 
						|
        base_parser.print_help()
 | 
						|
    exit(1)
 | 
						|
 | 
						|
 | 
						|
class Tracer(object):
 | 
						|
    def __init__(self, args, level=sys.maxsize):
 | 
						|
        self.args = args
 | 
						|
        self.max_level = level
 | 
						|
        self.indenter = source.Indenter()
 | 
						|
 | 
						|
    def build_node_tags(self, node):
 | 
						|
        pass
 | 
						|
 | 
						|
    def build_node(self, node):
 | 
						|
        if node[issues.JSON_INDEX_TRACE_LEVEL] > self.max_level:
 | 
						|
            return
 | 
						|
 | 
						|
        report_line = node[issues.JSON_INDEX_TRACE_LINE]
 | 
						|
        report_col = node[issues.JSON_INDEX_TRACE_COLUMN]
 | 
						|
        fname = node[issues.JSON_INDEX_TRACE_FILENAME]
 | 
						|
 | 
						|
        self.indenter.newline()
 | 
						|
        self.indenter.add('%s:%d:%d: %s' % (
 | 
						|
            fname,
 | 
						|
            report_line,
 | 
						|
            report_col,
 | 
						|
            node[issues.JSON_INDEX_TRACE_DESCRIPTION],
 | 
						|
        ))
 | 
						|
        self.indenter.newline()
 | 
						|
 | 
						|
        if not self.args.no_source:
 | 
						|
            self.indenter.indent_push(node[issues.JSON_INDEX_TRACE_LEVEL])
 | 
						|
            mode = colorize.TERMINAL_FORMATTER
 | 
						|
            if self.args.html:
 | 
						|
                mode = colorize.PLAIN_FORMATTER
 | 
						|
            empty_desc = len(node[issues.JSON_INDEX_TRACE_DESCRIPTION]) == 0
 | 
						|
            self.indenter.add(source.build_source_context(fname,
 | 
						|
                                                          mode,
 | 
						|
                                                          report_line,
 | 
						|
                                                          report_col,
 | 
						|
                                                          empty_desc
 | 
						|
                                                          ))
 | 
						|
            self.indenter.indent_pop()
 | 
						|
            self.indenter.newline()
 | 
						|
 | 
						|
    def build_trace(self, trace):
 | 
						|
        total_nodes = len(trace)
 | 
						|
        hidden_nodes = len(
 | 
						|
            filter(lambda n: n[issues.JSON_INDEX_TRACE_LEVEL] > self.max_level,
 | 
						|
                   trace))
 | 
						|
        shown_nodes = total_nodes - hidden_nodes
 | 
						|
        hidden_str = ''
 | 
						|
        all_str = 'all '
 | 
						|
        if hidden_nodes > 0:
 | 
						|
            hidden_str = ' (%d steps too deeply nested)' % hidden_nodes
 | 
						|
            all_str = ''
 | 
						|
        self.indenter.add('Showing %s%d steps of the trace%s\n\n'
 | 
						|
                          % (all_str, shown_nodes, hidden_str))
 | 
						|
        self.indenter.newline()
 | 
						|
 | 
						|
        for node in trace:
 | 
						|
            self.build_node(node)
 | 
						|
 | 
						|
    def build_report(self, report):
 | 
						|
        self.build_trace(report[issues.JSON_INDEX_TRACE])
 | 
						|
 | 
						|
    def __unicode__(self):
 | 
						|
        return unicode(self.indenter)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return str(self.indenter)
 | 
						|
 | 
						|
 | 
						|
class Selector(object):
 | 
						|
    def __init__(self, args, reports):
 | 
						|
        self.args = args
 | 
						|
 | 
						|
        def has_trace(report):
 | 
						|
            return len(report[issues.JSON_INDEX_TRACE]) > 0
 | 
						|
        self.reports = [report for report in reports if has_trace(report)]
 | 
						|
 | 
						|
    def show_choices(self):
 | 
						|
        n = 0
 | 
						|
        n_length = len(str(len(self)))
 | 
						|
        for report in self.reports:
 | 
						|
            # the goal is to get the following output for each report:
 | 
						|
            # 1234. <first line of report #1234 goes here>
 | 
						|
            #       <second line of report goes here>
 | 
						|
            msg = issues.text_of_report(report) \
 | 
						|
                        .replace('\n', '\n%s' % ((n_length + 2) * ' '))
 | 
						|
            utils.stdout('%s. %s\n' % (str(n).rjust(n_length), msg))
 | 
						|
            n += 1
 | 
						|
 | 
						|
    def prompt_report(self):
 | 
						|
        report_number = 0
 | 
						|
        if self.args.select is not None:
 | 
						|
            report_number = self.parse_report_number(self.args.select, True)
 | 
						|
        else:
 | 
						|
            self.show_choices()
 | 
						|
 | 
						|
            if len(self) > 1:
 | 
						|
                report_number_str = raw_input(
 | 
						|
                    'Choose report to display (default=0): ')
 | 
						|
                if report_number_str != '':
 | 
						|
                    report_number = self.parse_report_number(report_number_str)
 | 
						|
            elif len(self) == 1:
 | 
						|
                print('Auto-selecting the only report.')
 | 
						|
 | 
						|
        return self.reports[report_number]
 | 
						|
 | 
						|
    def prompt_level(self):
 | 
						|
        return self.parse_max_level(self.args.max_level, True)
 | 
						|
 | 
						|
    def parse_report_number(self, s, show_help=False):
 | 
						|
        try:
 | 
						|
            n = int(s)
 | 
						|
        except ValueError:
 | 
						|
            show_error_and_exit(
 | 
						|
                'ERROR: integer report number expected',
 | 
						|
                show_help)
 | 
						|
 | 
						|
        if n >= len(self) or n < 0:
 | 
						|
            show_error_and_exit('ERROR: invalid report number.', show_help)
 | 
						|
 | 
						|
        return n
 | 
						|
 | 
						|
    def parse_max_level(self, s, show_help=False):
 | 
						|
        if s is None:
 | 
						|
            return sys.maxsize
 | 
						|
 | 
						|
        try:
 | 
						|
            n = int(s)
 | 
						|
        except ValueError:
 | 
						|
            show_error_and_exit(
 | 
						|
                'ERROR: integer max level expected',
 | 
						|
                show_help)
 | 
						|
 | 
						|
        if n < 0:
 | 
						|
            show_error_and_exit('ERROR: invalid max level.', show_help)
 | 
						|
 | 
						|
        return n
 | 
						|
 | 
						|
    def __len__(self):
 | 
						|
        return len(self.reports)
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        return self.reports.__iter__()
 | 
						|
 | 
						|
    def __next__(self):
 | 
						|
        return self.reports.__next__()
 | 
						|
 | 
						|
 | 
						|
def path_of_bug_number(traces_dir, i):
 | 
						|
    return os.path.join(traces_dir, 'bug_%d.txt' % (i+1))
 | 
						|
 | 
						|
 | 
						|
def url_of_bug_number(i):
 | 
						|
    return '%s/bug_%d.txt' % (TRACES_REPORT_DIR, i+1)
 | 
						|
 | 
						|
 | 
						|
def get_remote_source_template():
 | 
						|
    """Return a template that given 'file-name' and 'line-number' entries
 | 
						|
    gives a remote url to that source location. Return the empty
 | 
						|
    template if no remote source has been detected. Currently only
 | 
						|
    detects GitHub projects.
 | 
						|
    """
 | 
						|
    # see if we are in a GitHub project clone
 | 
						|
    try:
 | 
						|
        git_remote = subprocess.check_output(
 | 
						|
            ['git',
 | 
						|
             'config',
 | 
						|
             '--get',
 | 
						|
             'remote.origin.url']).decode().strip()
 | 
						|
        m = SOURCE_REMOTE_GITHUB_RE.match(git_remote)
 | 
						|
        if m is not None:
 | 
						|
            project = m.group('project')
 | 
						|
            # some remotes end in .git, but the http urls don't have
 | 
						|
            # these
 | 
						|
            if project.endswith('.git'):
 | 
						|
                project = project[:-len('.git')]
 | 
						|
            utils.stdout('Detected GitHub project %s' % project)
 | 
						|
            hash = subprocess.check_output(
 | 
						|
                ['git',
 | 
						|
                 'rev-parse',
 | 
						|
                 'HEAD']).decode().strip()
 | 
						|
            root = subprocess.check_output(
 | 
						|
                ['git',
 | 
						|
                 'rev-parse',
 | 
						|
                 '--show-toplevel']).decode().strip()
 | 
						|
            # FIXME(t8921813): we should have a way to get absolute
 | 
						|
            # paths in traces. In the meantime, trust that we run from
 | 
						|
            # the same directory from which infer was run.
 | 
						|
            relative_path = os.path.relpath(os.getcwd(), root)
 | 
						|
            d = {
 | 
						|
                'project': project,
 | 
						|
                'hash': hash,
 | 
						|
                'relative-path': relative_path,
 | 
						|
                'file-name': '{file-name}',
 | 
						|
                'line-number': '{line-number}',
 | 
						|
            }
 | 
						|
            return SOURCE_REMOTE_GITHUB_URL_TEMPLATE.format(**d)
 | 
						|
    except subprocess.CalledProcessError:
 | 
						|
        pass
 | 
						|
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
def html_bug_trace(args, report, bug_id):
 | 
						|
    bug_trace = ''
 | 
						|
    bug_trace += '%s\n' % issues.text_of_report(report)
 | 
						|
    tracer = Tracer(args)
 | 
						|
    tracer.build_report(report)
 | 
						|
    bug_trace += unicode(tracer)
 | 
						|
    return bug_trace
 | 
						|
 | 
						|
 | 
						|
def html_list_of_bugs(args, remote_source_template, selector):
 | 
						|
    template = '\n'.join([
 | 
						|
        '<html>',
 | 
						|
        '<head>',
 | 
						|
        '<title>Infer found {num-bugs} bugs</title>',
 | 
						|
        '</head>',
 | 
						|
        '<body>',
 | 
						|
        '<h2>List of bugs found</h2>',
 | 
						|
        '{list-of-bugs}',
 | 
						|
        '</body>',
 | 
						|
        '</html>',
 | 
						|
    ])
 | 
						|
 | 
						|
    report_template = '\n'.join([
 | 
						|
        '<li>',
 | 
						|
        '{description}',
 | 
						|
        '({source-uri}<a href="{trace-url}">trace</a>)',
 | 
						|
        '</li>',
 | 
						|
    ])
 | 
						|
 | 
						|
    def source_uri(report):
 | 
						|
        d = {
 | 
						|
            'file-name': report[issues.JSON_INDEX_FILENAME],
 | 
						|
            'line-number': report[issues.JSON_INDEX_LINE],
 | 
						|
        }
 | 
						|
        if remote_source_template is not None:
 | 
						|
            link = remote_source_template.format(**d)
 | 
						|
            return '<a href="%s">source</a> | ' % link
 | 
						|
        return ''
 | 
						|
 | 
						|
    i = 0
 | 
						|
    list_of_bugs = '<ol>'
 | 
						|
    for report in selector:
 | 
						|
        d = {
 | 
						|
            'description': issues.text_of_report(report),
 | 
						|
            'trace-url': url_of_bug_number(i),
 | 
						|
            'source-uri': source_uri(report),
 | 
						|
        }
 | 
						|
        list_of_bugs += report_template.format(**d)
 | 
						|
        i += 1
 | 
						|
    list_of_bugs += '</ol>'
 | 
						|
 | 
						|
    d = {
 | 
						|
        'num-bugs': len(selector),
 | 
						|
        'list-of-bugs': list_of_bugs,
 | 
						|
    }
 | 
						|
    return template.format(**d)
 | 
						|
 | 
						|
 | 
						|
def generate_html_report(args, reports):
 | 
						|
    html_dir = os.path.join(args.infer_out, HTML_REPORT_DIR)
 | 
						|
    shutil.rmtree(html_dir, True)
 | 
						|
    utils.mkdir_if_not_exists(html_dir)
 | 
						|
 | 
						|
    traces_dir = os.path.join(html_dir, TRACES_REPORT_DIR)
 | 
						|
    utils.mkdir_if_not_exists(traces_dir)
 | 
						|
 | 
						|
    sel = Selector(args, reports)
 | 
						|
 | 
						|
    i = 0
 | 
						|
    for bug in sel:
 | 
						|
        bug_trace_path = path_of_bug_number(traces_dir, i)
 | 
						|
        with codecs.open(bug_trace_path, 'w',
 | 
						|
                         encoding=config.CODESET,
 | 
						|
                         errors='xmlcharrefreplace') as bug_trace_file:
 | 
						|
            bug_trace_file.write(html_bug_trace(args, bug, i))
 | 
						|
        i += 1
 | 
						|
 | 
						|
    remote_source_template = get_remote_source_template()
 | 
						|
    bug_list_path = os.path.join(html_dir, 'index.html')
 | 
						|
    with codecs.open(bug_list_path, 'w',
 | 
						|
                     encoding=config.CODESET,
 | 
						|
                     errors='xmlcharrefreplace') as bug_list_file:
 | 
						|
        bug_list_file.write(html_list_of_bugs(args,
 | 
						|
                                              remote_source_template,
 | 
						|
                                              sel))
 | 
						|
 | 
						|
    utils.stdout('Saved html report in:\n%s' % bug_list_path)
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
    args = base_parser.parse_args()
 | 
						|
 | 
						|
    report_filename = os.path.join(args.infer_out, config.JSON_REPORT_FILENAME)
 | 
						|
    reports = utils.load_json_from_path(report_filename)
 | 
						|
 | 
						|
    if args.html:
 | 
						|
        generate_html_report(args, reports)
 | 
						|
        exit(0)
 | 
						|
 | 
						|
    sel = Selector(args, reports)
 | 
						|
 | 
						|
    if len(sel) == 0:
 | 
						|
        print('No issues found')
 | 
						|
        exit(0)
 | 
						|
 | 
						|
    if args.only_show:
 | 
						|
        sel.show_choices()
 | 
						|
        exit(0)
 | 
						|
 | 
						|
    report = sel.prompt_report()
 | 
						|
    max_level = sel.prompt_level()
 | 
						|
 | 
						|
    utils.stdout(issues.text_of_report(report))
 | 
						|
 | 
						|
    tracer = Tracer(args, max_level)
 | 
						|
    tracer.build_report(report)
 | 
						|
    utils.stdout(unicode(tracer))
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    main()
 |