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