#!/usr/bin/python #---------------------------------------------------------------------- # Be sure to add the python path that points to the LLDB shared library. # # # To use this in the embedded python interpreter using "lldb" just # import it with the full path using the "command script import" # command # (lldb) command script import /path/to/clandiag.py #---------------------------------------------------------------------- from __future__ import absolute_import, division, print_function import lldb import argparse import shlex import os import re import subprocess class MyParser(argparse.ArgumentParser): def format_help(self): return ''' Commands for managing clang diagnostic breakpoints Syntax: clangdiag enable [|] clangdiag disable clangdiag diagtool [|reset] The following subcommands are supported: enable -- Enable clang diagnostic breakpoints. disable -- Disable all clang diagnostic breakpoints. diagtool -- Return, set, or reset diagtool path. This command sets breakpoints in clang, and clang based tools, that emit diagnostics. When a diagnostic is emitted, and clangdiag is enabled, it will use the appropriate diagtool application to determine the name of the DiagID, and set breakpoints in all locations that 'diag::name' appears in the source. Since the new breakpoints are set after they are encountered, users will need to launch the executable a second time in order to hit the new breakpoints. For in-tree builds, the diagtool application, used to map DiagID's to names, is found automatically in the same directory as the target executable. However, out-or-tree builds must use the 'diagtool' subcommand to set the appropriate path for diagtool in the clang debug bin directory. Since this mapping is created at build-time, it's important for users to use the same version that was generated when clang was compiled, or else the id's won't match. Notes: - Substrings can be passed for both and . - If is passed, only enable the DiagID(s) for that warning. - If is passed, only enable that DiagID. - Rerunning enable clears existing breakpoints. - diagtool is used in breakpoint callbacks, so it can be changed without the need to rerun enable. - Adding this to your ~.lldbinit file makes clangdiag available at startup: "command script import /path/to/clangdiag.py" ''' def create_diag_options(): parser = MyParser(prog='clangdiag') subparsers = parser.add_subparsers( title='subcommands', dest='subcommands', metavar='') disable_parser = subparsers.add_parser('disable') enable_parser = subparsers.add_parser('enable') enable_parser.add_argument('id', nargs='?') diagtool_parser = subparsers.add_parser('diagtool') diagtool_parser.add_argument('path', nargs='?') return parser def getDiagtool(target, diagtool = None): id = target.GetProcess().GetProcessID() if 'diagtool' not in getDiagtool.__dict__: getDiagtool.diagtool = {} if diagtool: if diagtool == 'reset': getDiagtool.diagtool[id] = None elif os.path.exists(diagtool): getDiagtool.diagtool[id] = diagtool else: print('clangdiag: %s not found.' % diagtool) if not id in getDiagtool.diagtool or not getDiagtool.diagtool[id]: getDiagtool.diagtool[id] = None exe = target.GetExecutable() if not exe.Exists(): print('clangdiag: Target (%s) not set.' % exe.GetFilename()) else: diagtool = os.path.join(exe.GetDirectory(), 'diagtool') if os.path.exists(diagtool): getDiagtool.diagtool[id] = diagtool else: print('clangdiag: diagtool not found along side %s' % exe) return getDiagtool.diagtool[id] def setDiagBreakpoint(frame, bp_loc, dict): id = frame.FindVariable("DiagID").GetValue() if id is None: print('clangdiag: id is None') return False # Don't need to test this time, since we did that in enable. target = frame.GetThread().GetProcess().GetTarget() diagtool = getDiagtool(target) name = subprocess.check_output([diagtool, "find-diagnostic-id", id]).rstrip(); # Make sure we only consider errors, warnings, and extensions. # FIXME: Make this configurable? prefixes = ['err_', 'warn_', 'exp_'] if len([prefix for prefix in prefixes+[''] if name.startswith(prefix)][0]): bp = target.BreakpointCreateBySourceRegex(name, lldb.SBFileSpec()) bp.AddName("clang::Diagnostic") return False def enable(exe_ctx, args): # Always disable existing breakpoints disable(exe_ctx) target = exe_ctx.GetTarget() numOfBreakpoints = target.GetNumBreakpoints() if args.id: # Make sure we only consider errors, warnings, and extensions. # FIXME: Make this configurable? prefixes = ['err_', 'warn_', 'exp_'] if len([prefix for prefix in prefixes+[''] if args.id.startswith(prefix)][0]): bp = target.BreakpointCreateBySourceRegex(args.id, lldb.SBFileSpec()) bp.AddName("clang::Diagnostic") else: diagtool = getDiagtool(target) list = subprocess.check_output([diagtool, "list-warnings"]).rstrip(); for line in list.splitlines(True): m = re.search(r' *(.*) .*\[\-W' + re.escape(args.id) + r'.*].*', line) # Make sure we only consider warnings. if m and m.group(1).startswith('warn_'): bp = target.BreakpointCreateBySourceRegex(m.group(1), lldb.SBFileSpec()) bp.AddName("clang::Diagnostic") else: print('Adding callbacks.') bp = target.BreakpointCreateByName('DiagnosticsEngine::Report') bp.SetScriptCallbackFunction('clangdiag.setDiagBreakpoint') bp.AddName("clang::Diagnostic") count = target.GetNumBreakpoints() - numOfBreakpoints print('%i breakpoint%s added.' % (count, "s"[count==1:])) return def disable(exe_ctx): target = exe_ctx.GetTarget() # Remove all diag breakpoints. bkpts = lldb.SBBreakpointList(target) target.FindBreakpointsByName("clang::Diagnostic", bkpts) for i in range(bkpts.GetSize()): target.BreakpointDelete(bkpts.GetBreakpointAtIndex(i).GetID()) return def the_diag_command(debugger, command, exe_ctx, result, dict): # Use the Shell Lexer to properly parse up command options just like a # shell would command_args = shlex.split(command) parser = create_diag_options() try: args = parser.parse_args(command_args) except: return if args.subcommands == 'enable': enable(exe_ctx, args) elif args.subcommands == 'disable': disable(exe_ctx) else: diagtool = getDiagtool(exe_ctx.GetTarget(), args.path) print('diagtool = %s' % diagtool) return def __lldb_init_module(debugger, dict): # This initializer is being run from LLDB in the embedded command interpreter # Make the options so we can generate the help text for the new LLDB # command line command prior to registering it with LLDB below parser = create_diag_options() the_diag_command.__doc__ = parser.format_help() # Add any commands contained in this module to LLDB debugger.HandleCommand( 'command script add -f clangdiag.the_diag_command clangdiag') print('The "clangdiag" command has been installed, type "help clangdiag" or "clangdiag --help" for detailed help.')