xref: /llvm-project/clang/utils/clangdiag.py (revision dd3c26a045c081620375a878159f536758baba6e)
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