1#!/usr/bin/env python 2 3# ---------------------------------------------------------------------- 4# Be sure to add the python path that points to the LLDB shared library. 5# 6# # To use this in the embedded python interpreter using "lldb" just 7# import it with the full path using the "command script import" 8# command 9# (lldb) command script import /path/to/clandiag.py 10# ---------------------------------------------------------------------- 11 12from __future__ import absolute_import, division, print_function 13import lldb 14import argparse 15import shlex 16import os 17import re 18import subprocess 19 20 21class MyParser(argparse.ArgumentParser): 22 def format_help(self): 23 return """ Commands for managing clang diagnostic breakpoints 24 25Syntax: clangdiag enable [<warning>|<diag-name>] 26 clangdiag disable 27 clangdiag diagtool [<path>|reset] 28 29The following subcommands are supported: 30 31 enable -- Enable clang diagnostic breakpoints. 32 disable -- Disable all clang diagnostic breakpoints. 33 diagtool -- Return, set, or reset diagtool path. 34 35This command sets breakpoints in clang, and clang based tools, that 36emit diagnostics. When a diagnostic is emitted, and clangdiag is 37enabled, it will use the appropriate diagtool application to determine 38the name of the DiagID, and set breakpoints in all locations that 39'diag::name' appears in the source. Since the new breakpoints are set 40after they are encountered, users will need to launch the executable a 41second time in order to hit the new breakpoints. 42 43For in-tree builds, the diagtool application, used to map DiagID's to 44names, is found automatically in the same directory as the target 45executable. However, out-or-tree builds must use the 'diagtool' 46subcommand to set the appropriate path for diagtool in the clang debug 47bin directory. Since this mapping is created at build-time, it's 48important for users to use the same version that was generated when 49clang was compiled, or else the id's won't match. 50 51Notes: 52- Substrings can be passed for both <warning> and <diag-name>. 53- If <warning> is passed, only enable the DiagID(s) for that warning. 54- If <diag-name> is passed, only enable that DiagID. 55- Rerunning enable clears existing breakpoints. 56- diagtool is used in breakpoint callbacks, so it can be changed 57 without the need to rerun enable. 58- Adding this to your ~.lldbinit file makes clangdiag available at startup: 59 "command script import /path/to/clangdiag.py" 60 61""" 62 63 64def create_diag_options(): 65 parser = MyParser(prog="clangdiag") 66 subparsers = parser.add_subparsers( 67 title="subcommands", dest="subcommands", metavar="" 68 ) 69 disable_parser = subparsers.add_parser("disable") 70 enable_parser = subparsers.add_parser("enable") 71 enable_parser.add_argument("id", nargs="?") 72 diagtool_parser = subparsers.add_parser("diagtool") 73 diagtool_parser.add_argument("path", nargs="?") 74 return parser 75 76 77def getDiagtool(target, diagtool=None): 78 id = target.GetProcess().GetProcessID() 79 if "diagtool" not in getDiagtool.__dict__: 80 getDiagtool.diagtool = {} 81 if diagtool: 82 if diagtool == "reset": 83 getDiagtool.diagtool[id] = None 84 elif os.path.exists(diagtool): 85 getDiagtool.diagtool[id] = diagtool 86 else: 87 print("clangdiag: %s not found." % diagtool) 88 if not id in getDiagtool.diagtool or not getDiagtool.diagtool[id]: 89 getDiagtool.diagtool[id] = None 90 exe = target.GetExecutable() 91 if not exe.Exists(): 92 print("clangdiag: Target (%s) not set." % exe.GetFilename()) 93 else: 94 diagtool = os.path.join(exe.GetDirectory(), "diagtool") 95 if os.path.exists(diagtool): 96 getDiagtool.diagtool[id] = diagtool 97 else: 98 print("clangdiag: diagtool not found along side %s" % exe) 99 100 return getDiagtool.diagtool[id] 101 102 103def setDiagBreakpoint(frame, bp_loc, dict): 104 id = frame.FindVariable("DiagID").GetValue() 105 if id is None: 106 print("clangdiag: id is None") 107 return False 108 109 # Don't need to test this time, since we did that in enable. 110 target = frame.GetThread().GetProcess().GetTarget() 111 diagtool = getDiagtool(target) 112 name = subprocess.check_output([diagtool, "find-diagnostic-id", id]).rstrip() 113 # Make sure we only consider errors, warnings, and extensions. 114 # FIXME: Make this configurable? 115 prefixes = ["err_", "warn_", "exp_"] 116 if len([prefix for prefix in prefixes + [""] if name.startswith(prefix)][0]): 117 bp = target.BreakpointCreateBySourceRegex(name, lldb.SBFileSpec()) 118 bp.AddName("clang::Diagnostic") 119 120 return False 121 122 123def enable(exe_ctx, args): 124 # Always disable existing breakpoints 125 disable(exe_ctx) 126 127 target = exe_ctx.GetTarget() 128 numOfBreakpoints = target.GetNumBreakpoints() 129 130 if args.id: 131 # Make sure we only consider errors, warnings, and extensions. 132 # FIXME: Make this configurable? 133 prefixes = ["err_", "warn_", "exp_"] 134 if len([prefix for prefix in prefixes + [""] if args.id.startswith(prefix)][0]): 135 bp = target.BreakpointCreateBySourceRegex(args.id, lldb.SBFileSpec()) 136 bp.AddName("clang::Diagnostic") 137 else: 138 diagtool = getDiagtool(target) 139 list = subprocess.check_output([diagtool, "list-warnings"]).rstrip() 140 for line in list.splitlines(True): 141 m = re.search(r" *(.*) .*\[\-W" + re.escape(args.id) + r".*].*", line) 142 # Make sure we only consider warnings. 143 if m and m.group(1).startswith("warn_"): 144 bp = target.BreakpointCreateBySourceRegex( 145 m.group(1), lldb.SBFileSpec() 146 ) 147 bp.AddName("clang::Diagnostic") 148 else: 149 print("Adding callbacks.") 150 bp = target.BreakpointCreateByName("DiagnosticsEngine::Report") 151 bp.SetScriptCallbackFunction("clangdiag.setDiagBreakpoint") 152 bp.AddName("clang::Diagnostic") 153 154 count = target.GetNumBreakpoints() - numOfBreakpoints 155 print("%i breakpoint%s added." % (count, "s"[count == 1 :])) 156 157 return 158 159 160def disable(exe_ctx): 161 target = exe_ctx.GetTarget() 162 # Remove all diag breakpoints. 163 bkpts = lldb.SBBreakpointList(target) 164 target.FindBreakpointsByName("clang::Diagnostic", bkpts) 165 for i in range(bkpts.GetSize()): 166 target.BreakpointDelete(bkpts.GetBreakpointAtIndex(i).GetID()) 167 168 return 169 170 171def the_diag_command(debugger, command, exe_ctx, result, dict): 172 # Use the Shell Lexer to properly parse up command options just like a 173 # shell would 174 command_args = shlex.split(command) 175 parser = create_diag_options() 176 try: 177 args = parser.parse_args(command_args) 178 except: 179 return 180 181 if args.subcommands == "enable": 182 enable(exe_ctx, args) 183 elif args.subcommands == "disable": 184 disable(exe_ctx) 185 else: 186 diagtool = getDiagtool(exe_ctx.GetTarget(), args.path) 187 print("diagtool = %s" % diagtool) 188 189 return 190 191 192def __lldb_init_module(debugger, dict): 193 # This initializer is being run from LLDB in the embedded command interpreter 194 # Make the options so we can generate the help text for the new LLDB 195 # command line command prior to registering it with LLDB below 196 parser = create_diag_options() 197 the_diag_command.__doc__ = parser.format_help() 198 # Add any commands contained in this module to LLDB 199 debugger.HandleCommand("command script add -f clangdiag.the_diag_command clangdiag") 200 print( 201 'The "clangdiag" command has been installed, type "help clangdiag" or "clangdiag --help" for detailed help.' 202 ) 203