193 lines
7.4 KiB
Python
193 lines
7.4 KiB
Python
|
#!/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.')
|