| #!/usr/bin/env 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 [<warning>|<diag-name>] |
| clangdiag disable |
| clangdiag diagtool [<path>|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 <warning> and <diag-name>. |
| - If <warning> is passed, only enable the DiagID(s) for that warning. |
| - If <diag-name> 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.') |