ljclang

FORK: A LuaJIT-based interface to libclang
git clone https://git.neptards.moe/neptards/ljclang.git
Log | Files | Refs

mgrep.lua (13044B)


      1 #!/usr/bin/env luajit
      2 -- mgrep.lua -- Search for named member accesses.
      3 
      4 local io = require("io")
      5 local os = require("os")
      6 local math = require("math")
      7 local string = require("string")
      8 local table = require("table")
      9 
     10 local jit = require("jit")
     11 local ffi = require("ffi")
     12 local C = ffi.C
     13 
     14 local cl = require("ljclang")
     15 local class = require("class").class
     16 
     17 local abs = math.abs
     18 local format = string.format
     19 
     20 local assert = assert
     21 local print = print
     22 
     23 ffi.cdef[[
     24 char *getcwd(char *buf, size_t size);
     25 void free(void *ptr);
     26 ]]
     27 
     28 local function getcwd()
     29     if (jit.os ~= "Linux") then
     30         return nil
     31     end
     32 
     33     local cwd = C.getcwd(nil, 0)
     34     if (cwd == nil) then
     35         return nil
     36     end
     37 
     38     local str = ffi.string(cwd)
     39     C.free(cwd)
     40     return str
     41 end
     42 
     43 ----------
     44 
     45 local function printf(fmt, ...)
     46     print(format(fmt, ...))
     47 end
     48 
     49 local function errprint(str)
     50     io.stderr:write(str.."\n")
     51 end
     52 
     53 local function errprintf(fmt, ...)
     54     errprint(format(fmt, ...))
     55 end
     56 
     57 local function abort(str)
     58     errprint(str.."\n")
     59     os.exit(1)
     60 end
     61 
     62 local function usage(hline)
     63     if (hline) then
     64         errprint("ERROR: "..hline.."\n")
     65     end
     66     local progname = arg[0]:match("([^/]+)$")
     67     errprint("Usage:\n  "..progname.." <typeName>::<memberName> [options...] [filenames...]\n")
     68     errprint
     69 [[
     70 Options:
     71   -d /path/to/compile_commands.json: use compilation database
     72   -O '<clang_options...>': pass options to Clang, split at whitespace
     73   --no-color: Turn off match and diagnostic highlighting
     74   -n: only parse and potentially print diagnostics
     75   -q: be quiet (don't print diagnostics)
     76 
     77   For the compilation DB invocation, -O can be used for e.g. -I./clang-include (-> /usr/local/lib/clang/3.7.0/include)
     78   (Workaround for -isystem and -I/usr/local/lib/clang/3.7.0/include not working)
     79 ]]
     80     os.exit(1)
     81 end
     82 
     83 if (arg[1] == nil) then
     84     usage()
     85 end
     86 
     87 local parsecmdline = require("parsecmdline_pk")
     88 local opt_meta = {
     89     [0] = -1,
     90     d=true, O=true, q=false, n=false,
     91     ["-no-color"]=false
     92 }
     93 
     94 local opts, files = parsecmdline.getopts(opt_meta, arg, usage)
     95 
     96 local compDbName = opts.d
     97 local clangOpts = opts.O
     98 local quiet = opts.q
     99 local dryrun = opts.n
    100 local useColors = not opts["-no-color"]
    101 
    102 local queryStr = files[0]
    103 files[0] = nil
    104 
    105 if (queryStr == nil) then
    106     usage("Must provide <typeName>::<memberName> to search for as first argument")
    107 end
    108 
    109 local function parseQueryString(qstr)
    110     local pos = qstr:find("::")
    111     if (pos == nil) then
    112         abort("ERROR: member to search for must be specified as <typeName>::<memberName>")
    113     end
    114 
    115     return qstr:sub(1,pos-1), qstr:sub(pos+2)
    116 end
    117 
    118 local typeName, memberName = parseQueryString(queryStr)
    119 
    120 local g_curFileName
    121 -- The ClassDecl or StructDecl of the type we're searching for:
    122 local g_structDecl
    123 local g_cursorKind
    124 
    125 local V = cl.ChildVisitResult
    126 -- Visitor for finding the named structure declaration.
    127 local GetTypeVisitor = cl.regCursorVisitor(
    128 function(cur, parent)
    129     local curKind = cur:kind()
    130 
    131     if (curKind == "ClassDecl" or curKind == "StructDecl") then
    132         if (cur:name() == typeName) then
    133             g_structDecl = cl.Cursor(cur)
    134             g_cursorKind = curKind
    135             return V.Break
    136         end
    137     elseif (curKind == "TypedefDecl") then
    138         local typ = cur:typedefType()
    139         local structDecl = typ:declaration()
    140         if (structDecl:haskind("StructDecl")) then
    141 --            printf("typedef struct %s %s", structDecl:name(), cur:name())
    142             if (cur:name() == typeName) then
    143                 g_structDecl = structDecl
    144                 g_cursorKind = "StructDecl"
    145                 return V.Break
    146             end
    147         end
    148     end
    149 
    150     return V.Continue
    151 end)
    152 
    153 --------------------
    154 
    155 -- For reference:
    156 -- https://wiki.archlinux.org/index.php/Color_Bash_Prompt#List_of_colors_for_prompt_and_Bash
    157 
    158 --local Normal = "0;"
    159 local Bold = "1;"
    160 --local Uline = "4;"
    161 
    162 --local Black = "30m"
    163 local Red = "31m"
    164 --local Green = "32m"
    165 --local Yellow = "33m"
    166 --local Blue = "34m"
    167 local Purple = "35m"
    168 --local Cyan = "36m"
    169 local White = "37m"
    170 
    171 local function colorize(str, modcolor)
    172     return "\027["..modcolor..str.."\027[m"
    173 end
    174 
    175 --------------------
    176 
    177 local SourceFile = class
    178 {
    179     function(fn)
    180         local fh, msg = io.open(fn)
    181         if (fh == nil) then
    182             errprintf("Could not open %s", msg)
    183             os.exit(1)
    184         end
    185 
    186         return { fh=fh, line=0 }
    187     end,
    188 
    189     __gc = function(f)
    190         f.fh:close()
    191     end,
    192 
    193     getLine = function(f, line)
    194         assert(f.line < line)
    195 
    196         local str
    197         while (f.line < line) do
    198             f.line = f.line+1
    199             str = f.fh:read("*l")
    200         end
    201 
    202         return str
    203     end,
    204 }
    205 
    206 local g_fileIdx = {}  -- [fileName] = fileIdx
    207 local g_fileName = {}  -- [fileIdx] = fileName
    208 local g_fileLines = {}  -- [fileIdx] = { linenum1, linenum2, ... }, negated if spans >1 line
    209 local g_fileColumnPairs = {}  -- [fileIdx] = { {colBeg1, colEnd1, colBeg2, colEnd2}, ... }
    210 
    211 local function clearResults()
    212     g_fileIdx = {}
    213     g_fileName = {}
    214     g_fileLines = {}
    215     g_fileColumnPairs = {}
    216 end
    217 
    218 -- Visitor for looking for the wanted member accesses.
    219 local SearchVisitor = cl.regCursorVisitor(
    220 function(cur, parent)
    221     if (cur:haskind("MemberRefExpr")) then
    222         local membname = cur:name()
    223         if (membname == memberName) then
    224             local def = cur:definition():parent()
    225             if (def:haskind(g_cursorKind) and def == g_structDecl) then
    226                 local fn, line, col, lineEnd, colEnd = cur:location()
    227                 local oneline = (line == lineEnd)
    228 
    229                 local idx = g_fileIdx[fn] or #g_fileLines+1
    230                 if (g_fileLines[idx] == nil) then
    231                     -- encountering file name for the first time
    232                     g_fileIdx[fn] = idx
    233                     g_fileName[idx] = fn
    234                     g_fileLines[idx] = {}
    235                     g_fileColumnPairs[idx] = {}
    236                 end
    237 
    238                 local lines = g_fileLines[idx]
    239                 local haveLine =
    240                     lines[#lines] ~= nil
    241                     and (abs(lines[#lines]) == line)
    242 
    243                 local lidx = haveLine and #lines or #lines+1
    244 
    245                 if (not haveLine) then
    246                     lines[lidx] = oneline and line or -line
    247                 end
    248 
    249                 local colNumPairs = g_fileColumnPairs[idx]
    250                 if (colNumPairs[lidx] == nil) then
    251                     colNumPairs[lidx] = {}
    252                 end
    253 
    254                 local pairs = colNumPairs[lidx]
    255                 if (oneline) then
    256                     -- The following 'if' check is to prevent adding the same
    257                     -- column pair twice, e.g. when a macro contains multiple
    258                     -- references to the searched-for member.
    259                     if (pairs[#pairs-1] ~= col) then
    260                         pairs[#pairs+1] = col
    261                         pairs[#pairs+1] = colEnd
    262                     end
    263                 end
    264             end
    265         end
    266     end
    267 
    268     return V.Recurse
    269 end)
    270 
    271 local function colorizeResult(str, colBegEnds)
    272     local a=1
    273     local strtab = {}
    274 
    275     for i=1,#colBegEnds,2 do
    276         local b = colBegEnds[i]
    277         local e = colBegEnds[i+1]
    278 
    279         strtab[#strtab+1] = str:sub(a,b-1)
    280         strtab[#strtab+1] = colorize(str:sub(b,e-1), Bold..Red)
    281         a = e
    282     end
    283     strtab[#strtab+1] = str:sub(a)
    284 
    285     return table.concat(strtab)
    286 end
    287 
    288 local curDir = getcwd()
    289 
    290 local function printResults()
    291     for fi = 1,#g_fileName do
    292         local fn = g_fileName[fi]
    293 
    294         if (curDir ~= nil and fn:sub(1,#curDir)==curDir) then
    295             fn = "./"..fn:sub(#curDir+2)
    296         end
    297 
    298         local lines = g_fileLines[fi]
    299         local pairs = g_fileColumnPairs[fi]
    300 
    301         local f = SourceFile(fn)
    302 
    303         for li=1,#lines do
    304 --            local oneline = (lines[li] > 0)
    305             local line = abs(lines[li])
    306             local str = f:getLine(line)
    307             if (useColors) then
    308                 str = colorizeResult(str, pairs[li])
    309             end
    310             printf("%s:%d: %s", fn, line, str)
    311         end
    312     end
    313 end
    314 
    315 local function getAllSourceFiles(db)
    316     local cmds = db:getAllCompileCommands()
    317 
    318     local haveSrc = {}
    319     local srcFiles = {}
    320 
    321     for ci=1,#cmds do
    322         local fns = cmds[ci]:getSourcePaths()
    323         for j=1,#fns do
    324             local fn = fns[j]
    325             if (not haveSrc[fn]) then
    326                 haveSrc[fn] = true
    327                 srcFiles[#srcFiles+1] = fn
    328             end
    329         end
    330     end
    331 
    332     return srcFiles
    333 end
    334 
    335 -- Make "-Irelative/subdir" -> "-I/path/to/relative/subdir",
    336 -- <opts> is modified in-place.
    337 local function absifyIncOpts(opts, prefixDir)
    338     if (prefixDir:sub(1,1) ~= "/") then
    339         -- XXX: Windows.
    340         errprintf("mgrep.lua: prefixDir '%s' does not start with '/'.", prefixDir)
    341         os.exit(1)
    342     end
    343 
    344     for i=1,#opts do
    345         local opt = opts[i]
    346         if (opt:sub(1,2)=="-I" and opt:sub(3,3)~="/") then
    347             opts[i] = "-I" .. prefixDir .. "/" .. opt:sub(3)
    348         end
    349     end
    350 
    351     return opts
    352 end
    353 
    354 -- Use a compilation database?
    355 local useCompDb = (compDbName ~= nil)
    356 local compArgs = {}  -- if using compDB, will have #compArgs == #files, each a table
    357 
    358 if (not useCompDb and #files == 0) then
    359     os.exit(0)
    360 end
    361 
    362 if (useCompDb) then
    363     if (#files > 0) then
    364         usage("When using compilation database, must pass no file names")
    365     end
    366 
    367     local compDbPos = compDbName:find("[\\/]compile_commands.json$")
    368     if (compDbPos == nil) then
    369         usage("File name of compilation database must be compile_commands.json")
    370     end
    371 
    372     local compDbDir = compDbName:sub(1, compDbPos)
    373     local db = cl.CompilationDatabase(compDbDir)
    374 
    375     if (db == nil) then
    376         abort("Fatal: Could not load compilation database")
    377     end
    378 
    379     -- Get all source files, uniq'd.
    380     files = getAllSourceFiles(db)
    381 
    382     if (#files == 0) then
    383         -- NOTE: We may get a CompilationDatabase even if
    384         -- clang_CompilationDatabase_fromDirectory() failed (as evidenced by
    385         -- error output from "LIBCLANG TOOLING").
    386         abort("Fatal: Compilation database contains no entries, or an error occurred")
    387     end
    388 
    389     for fi=1,#files do
    390         local cmds = db:getCompileCommands(files[fi])
    391         -- NOTE: Only use the first CompileCommand for a given file name:
    392         local cmd = cmds[1]
    393 
    394         -- NOTE: Strip "-c" and "-o" options from args. (else: "crash detected" for me)
    395         local args = cl.stripArgs(cmd:getArgs(false), "^-[co]$", 2)
    396         absifyIncOpts(args, cmd:getDirectory())
    397 
    398         -- Regarding "fatal error: 'stddef.h' file not found": for me,
    399         --  * -isystem didn't work ("crash detected").
    400         --  * -I/usr/local/lib/clang/3.7.0/include didn't work either
    401         --    (no effect)
    402         --  * -I<symlink to /usr/local/lib/clang/3.7.0/include> did work...
    403 
    404         if (clangOpts ~= nil) then
    405             local suffixArgs = cl.splitAtWhitespace(clangOpts)
    406             for ai=1,#suffixArgs do
    407                 args[#args+1] = suffixArgs[ai]
    408             end
    409         end
    410         compArgs[fi] = args
    411 
    412 --        if (fi == 1) then print("Args: "..table.concat(args, ', ').."\n") end
    413     end
    414 end
    415 
    416 local function GetColorizeTripleFunc(color)
    417     return function(pre, tag, post)
    418         return
    419             colorize(pre, Bold..White)..
    420             colorize(tag, Bold..color)..
    421             colorize(post, Bold..White)
    422     end
    423 end
    424 
    425 local ColorizeErrorFunc = GetColorizeTripleFunc(Red)
    426 local ColorizeWarningFunc = GetColorizeTripleFunc(Purple)
    427 
    428 for fi=1,#files do
    429     local fn = files[fi]
    430     g_curFileName = fn
    431 
    432     local index = cl.createIndex(true, false)
    433     local opts = useCompDb and compArgs[fi] or clangOpts or {}
    434 
    435     do
    436         local f, msg = io.open(fn)
    437         if (f == nil) then
    438             errprintf("ERROR: Failed opening %s", msg)
    439             goto nextfile
    440         end
    441         f:close()
    442     end
    443 
    444     local tu = index:parse(fn, opts)
    445 
    446     if (tu == nil) then
    447         errprintf("ERROR: Failed parsing %s", fn)
    448         goto nextfile
    449     end
    450 
    451     if (not quiet) then
    452         local diags = tu:diagnostics()
    453         for i=1,#diags do
    454             local text = diags[i].text
    455             if (useColors) then
    456                 text = text:gsub("(.*)(error: )(.*)", ColorizeErrorFunc)
    457                 text = text:gsub("(.*)(warning: )(.*)", ColorizeWarningFunc)
    458             end
    459             errprintf("%s", text)
    460         end
    461     end
    462 
    463     if (not dryrun) then
    464         local tuCursor = tu:cursor()
    465         tuCursor:children(GetTypeVisitor)
    466 
    467         if (g_structDecl == nil) then
    468             if (not quiet and not useCompDb) then
    469                 -- XXX: This is kind of noisy even in non-DB
    470                 -- mode. E.g. "mgrep.lua *.c": some C files may not include a
    471                 -- particular header.
    472                 errprintf("%s: Didn't find declaration for '%s'", fn, typeName)
    473             end
    474         else
    475             tuCursor:children(SearchVisitor)
    476             printResults()
    477             clearResults()
    478             g_structDecl = nil
    479             g_cursorKind = nil
    480         end
    481     end
    482 
    483     ::nextfile::
    484 end