You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
485 lines
13 KiB
Lua
485 lines
13 KiB
Lua
#!/usr/bin/env luajit
|
|
-- mgrep.lua -- Search for named member accesses.
|
|
|
|
local io = require("io")
|
|
local os = require("os")
|
|
local math = require("math")
|
|
local string = require("string")
|
|
local table = require("table")
|
|
|
|
local jit = require("jit")
|
|
local ffi = require("ffi")
|
|
local C = ffi.C
|
|
|
|
local cl = require("ljclang")
|
|
local class = require("class").class
|
|
|
|
local abs = math.abs
|
|
local format = string.format
|
|
|
|
local assert = assert
|
|
local print = print
|
|
|
|
ffi.cdef[[
|
|
char *getcwd(char *buf, size_t size);
|
|
void free(void *ptr);
|
|
]]
|
|
|
|
local function getcwd()
|
|
if (jit.os ~= "Linux") then
|
|
return nil
|
|
end
|
|
|
|
local cwd = C.getcwd(nil, 0)
|
|
if (cwd == nil) then
|
|
return nil
|
|
end
|
|
|
|
local str = ffi.string(cwd)
|
|
C.free(cwd)
|
|
return str
|
|
end
|
|
|
|
----------
|
|
|
|
local function printf(fmt, ...)
|
|
print(format(fmt, ...))
|
|
end
|
|
|
|
local function errprint(str)
|
|
io.stderr:write(str.."\n")
|
|
end
|
|
|
|
local function errprintf(fmt, ...)
|
|
errprint(format(fmt, ...))
|
|
end
|
|
|
|
local function abort(str)
|
|
errprint(str.."\n")
|
|
os.exit(1)
|
|
end
|
|
|
|
local function usage(hline)
|
|
if (hline) then
|
|
errprint("ERROR: "..hline.."\n")
|
|
end
|
|
local progname = arg[0]:match("([^/]+)$")
|
|
errprint("Usage:\n "..progname.." <typeName>::<memberName> [options...] [filenames...]\n")
|
|
errprint
|
|
[[
|
|
Options:
|
|
-d /path/to/compile_commands.json: use compilation database
|
|
-O '<clang_options...>': pass options to Clang, split at whitespace
|
|
--no-color: Turn off match and diagnostic highlighting
|
|
-n: only parse and potentially print diagnostics
|
|
-q: be quiet (don't print diagnostics)
|
|
|
|
For the compilation DB invocation, -O can be used for e.g. -I./clang-include (-> /usr/local/lib/clang/3.7.0/include)
|
|
(Workaround for -isystem and -I/usr/local/lib/clang/3.7.0/include not working)
|
|
]]
|
|
os.exit(1)
|
|
end
|
|
|
|
if (arg[1] == nil) then
|
|
usage()
|
|
end
|
|
|
|
local parsecmdline = require("parsecmdline_pk")
|
|
local opt_meta = {
|
|
[0] = -1,
|
|
d=true, O=true, q=false, n=false,
|
|
["-no-color"]=false
|
|
}
|
|
|
|
local opts, files = parsecmdline.getopts(opt_meta, arg, usage)
|
|
|
|
local compDbName = opts.d
|
|
local clangOpts = opts.O
|
|
local quiet = opts.q
|
|
local dryrun = opts.n
|
|
local useColors = not opts["-no-color"]
|
|
|
|
local queryStr = files[0]
|
|
files[0] = nil
|
|
|
|
if (queryStr == nil) then
|
|
usage("Must provide <typeName>::<memberName> to search for as first argument")
|
|
end
|
|
|
|
local function parseQueryString(qstr)
|
|
local pos = qstr:find("::")
|
|
if (pos == nil) then
|
|
abort("ERROR: member to search for must be specified as <typeName>::<memberName>")
|
|
end
|
|
|
|
return qstr:sub(1,pos-1), qstr:sub(pos+2)
|
|
end
|
|
|
|
local typeName, memberName = parseQueryString(queryStr)
|
|
|
|
local g_curFileName
|
|
-- The ClassDecl or StructDecl of the type we're searching for:
|
|
local g_structDecl
|
|
local g_cursorKind
|
|
|
|
local V = cl.ChildVisitResult
|
|
-- Visitor for finding the named structure declaration.
|
|
local GetTypeVisitor = cl.regCursorVisitor(
|
|
function(cur, parent)
|
|
local curKind = cur:kind()
|
|
|
|
if (curKind == "ClassDecl" or curKind == "StructDecl") then
|
|
if (cur:name() == typeName) then
|
|
g_structDecl = cl.Cursor(cur)
|
|
g_cursorKind = curKind
|
|
return V.Break
|
|
end
|
|
elseif (curKind == "TypedefDecl") then
|
|
local typ = cur:typedefType()
|
|
local structDecl = typ:declaration()
|
|
if (structDecl:haskind("StructDecl")) then
|
|
-- printf("typedef struct %s %s", structDecl:name(), cur:name())
|
|
if (cur:name() == typeName) then
|
|
g_structDecl = structDecl
|
|
g_cursorKind = "StructDecl"
|
|
return V.Break
|
|
end
|
|
end
|
|
end
|
|
|
|
return V.Continue
|
|
end)
|
|
|
|
--------------------
|
|
|
|
-- For reference:
|
|
-- https://wiki.archlinux.org/index.php/Color_Bash_Prompt#List_of_colors_for_prompt_and_Bash
|
|
|
|
--local Normal = "0;"
|
|
local Bold = "1;"
|
|
--local Uline = "4;"
|
|
|
|
--local Black = "30m"
|
|
local Red = "31m"
|
|
--local Green = "32m"
|
|
--local Yellow = "33m"
|
|
--local Blue = "34m"
|
|
local Purple = "35m"
|
|
--local Cyan = "36m"
|
|
local White = "37m"
|
|
|
|
local function colorize(str, modcolor)
|
|
return "\027["..modcolor..str.."\027[m"
|
|
end
|
|
|
|
--------------------
|
|
|
|
local SourceFile = class
|
|
{
|
|
function(fn)
|
|
local fh, msg = io.open(fn)
|
|
if (fh == nil) then
|
|
errprintf("Could not open %s", msg)
|
|
os.exit(1)
|
|
end
|
|
|
|
return { fh=fh, line=0 }
|
|
end,
|
|
|
|
__gc = function(f)
|
|
f.fh:close()
|
|
end,
|
|
|
|
getLine = function(f, line)
|
|
assert(f.line < line)
|
|
|
|
local str
|
|
while (f.line < line) do
|
|
f.line = f.line+1
|
|
str = f.fh:read("*l")
|
|
end
|
|
|
|
return str
|
|
end,
|
|
}
|
|
|
|
local g_fileIdx = {} -- [fileName] = fileIdx
|
|
local g_fileName = {} -- [fileIdx] = fileName
|
|
local g_fileLines = {} -- [fileIdx] = { linenum1, linenum2, ... }, negated if spans >1 line
|
|
local g_fileColumnPairs = {} -- [fileIdx] = { {colBeg1, colEnd1, colBeg2, colEnd2}, ... }
|
|
|
|
local function clearResults()
|
|
g_fileIdx = {}
|
|
g_fileName = {}
|
|
g_fileLines = {}
|
|
g_fileColumnPairs = {}
|
|
end
|
|
|
|
-- Visitor for looking for the wanted member accesses.
|
|
local SearchVisitor = cl.regCursorVisitor(
|
|
function(cur, parent)
|
|
if (cur:haskind("MemberRefExpr")) then
|
|
local membname = cur:name()
|
|
if (membname == memberName) then
|
|
local def = cur:definition():parent()
|
|
if (def:haskind(g_cursorKind) and def == g_structDecl) then
|
|
local fn, line, col, lineEnd, colEnd = cur:location()
|
|
local oneline = (line == lineEnd)
|
|
|
|
local idx = g_fileIdx[fn] or #g_fileLines+1
|
|
if (g_fileLines[idx] == nil) then
|
|
-- encountering file name for the first time
|
|
g_fileIdx[fn] = idx
|
|
g_fileName[idx] = fn
|
|
g_fileLines[idx] = {}
|
|
g_fileColumnPairs[idx] = {}
|
|
end
|
|
|
|
local lines = g_fileLines[idx]
|
|
local haveLine =
|
|
lines[#lines] ~= nil
|
|
and (abs(lines[#lines]) == line)
|
|
|
|
local lidx = haveLine and #lines or #lines+1
|
|
|
|
if (not haveLine) then
|
|
lines[lidx] = oneline and line or -line
|
|
end
|
|
|
|
local colNumPairs = g_fileColumnPairs[idx]
|
|
if (colNumPairs[lidx] == nil) then
|
|
colNumPairs[lidx] = {}
|
|
end
|
|
|
|
local pairs = colNumPairs[lidx]
|
|
if (oneline) then
|
|
-- The following 'if' check is to prevent adding the same
|
|
-- column pair twice, e.g. when a macro contains multiple
|
|
-- references to the searched-for member.
|
|
if (pairs[#pairs-1] ~= col) then
|
|
pairs[#pairs+1] = col
|
|
pairs[#pairs+1] = colEnd
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return V.Recurse
|
|
end)
|
|
|
|
local function colorizeResult(str, colBegEnds)
|
|
local a=1
|
|
local strtab = {}
|
|
|
|
for i=1,#colBegEnds,2 do
|
|
local b = colBegEnds[i]
|
|
local e = colBegEnds[i+1]
|
|
|
|
strtab[#strtab+1] = str:sub(a,b-1)
|
|
strtab[#strtab+1] = colorize(str:sub(b,e-1), Bold..Red)
|
|
a = e
|
|
end
|
|
strtab[#strtab+1] = str:sub(a)
|
|
|
|
return table.concat(strtab)
|
|
end
|
|
|
|
local curDir = getcwd()
|
|
|
|
local function printResults()
|
|
for fi = 1,#g_fileName do
|
|
local fn = g_fileName[fi]
|
|
|
|
if (curDir ~= nil and fn:sub(1,#curDir)==curDir) then
|
|
fn = "./"..fn:sub(#curDir+2)
|
|
end
|
|
|
|
local lines = g_fileLines[fi]
|
|
local pairs = g_fileColumnPairs[fi]
|
|
|
|
local f = SourceFile(fn)
|
|
|
|
for li=1,#lines do
|
|
-- local oneline = (lines[li] > 0)
|
|
local line = abs(lines[li])
|
|
local str = f:getLine(line)
|
|
if (useColors) then
|
|
str = colorizeResult(str, pairs[li])
|
|
end
|
|
printf("%s:%d: %s", fn, line, str)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function getAllSourceFiles(db)
|
|
local cmds = db:getAllCompileCommands()
|
|
|
|
local haveSrc = {}
|
|
local srcFiles = {}
|
|
|
|
for ci=1,#cmds do
|
|
local fns = cmds[ci]:getSourcePaths()
|
|
for j=1,#fns do
|
|
local fn = fns[j]
|
|
if (not haveSrc[fn]) then
|
|
haveSrc[fn] = true
|
|
srcFiles[#srcFiles+1] = fn
|
|
end
|
|
end
|
|
end
|
|
|
|
return srcFiles
|
|
end
|
|
|
|
-- Make "-Irelative/subdir" -> "-I/path/to/relative/subdir",
|
|
-- <opts> is modified in-place.
|
|
local function absifyIncOpts(opts, prefixDir)
|
|
if (prefixDir:sub(1,1) ~= "/") then
|
|
-- XXX: Windows.
|
|
errprintf("mgrep.lua: prefixDir '%s' does not start with '/'.", prefixDir)
|
|
os.exit(1)
|
|
end
|
|
|
|
for i=1,#opts do
|
|
local opt = opts[i]
|
|
if (opt:sub(1,2)=="-I" and opt:sub(3,3)~="/") then
|
|
opts[i] = "-I" .. prefixDir .. "/" .. opt:sub(3)
|
|
end
|
|
end
|
|
|
|
return opts
|
|
end
|
|
|
|
-- Use a compilation database?
|
|
local useCompDb = (compDbName ~= nil)
|
|
local compArgs = {} -- if using compDB, will have #compArgs == #files, each a table
|
|
|
|
if (not useCompDb and #files == 0) then
|
|
os.exit(0)
|
|
end
|
|
|
|
if (useCompDb) then
|
|
if (#files > 0) then
|
|
usage("When using compilation database, must pass no file names")
|
|
end
|
|
|
|
local compDbPos = compDbName:find("[\\/]compile_commands.json$")
|
|
if (compDbPos == nil) then
|
|
usage("File name of compilation database must be compile_commands.json")
|
|
end
|
|
|
|
local compDbDir = compDbName:sub(1, compDbPos)
|
|
local db = cl.CompilationDatabase(compDbDir)
|
|
|
|
if (db == nil) then
|
|
abort("Fatal: Could not load compilation database")
|
|
end
|
|
|
|
-- Get all source files, uniq'd.
|
|
files = getAllSourceFiles(db)
|
|
|
|
if (#files == 0) then
|
|
-- NOTE: We may get a CompilationDatabase even if
|
|
-- clang_CompilationDatabase_fromDirectory() failed (as evidenced by
|
|
-- error output from "LIBCLANG TOOLING").
|
|
abort("Fatal: Compilation database contains no entries, or an error occurred")
|
|
end
|
|
|
|
for fi=1,#files do
|
|
local cmds = db:getCompileCommands(files[fi])
|
|
-- NOTE: Only use the first CompileCommand for a given file name:
|
|
local cmd = cmds[1]
|
|
|
|
-- NOTE: Strip "-c" and "-o" options from args. (else: "crash detected" for me)
|
|
local args = cl.stripArgs(cmd:getArgs(false), "^-[co]$", 2)
|
|
absifyIncOpts(args, cmd:getDirectory())
|
|
|
|
-- Regarding "fatal error: 'stddef.h' file not found": for me,
|
|
-- * -isystem didn't work ("crash detected").
|
|
-- * -I/usr/local/lib/clang/3.7.0/include didn't work either
|
|
-- (no effect)
|
|
-- * -I<symlink to /usr/local/lib/clang/3.7.0/include> did work...
|
|
|
|
if (clangOpts ~= nil) then
|
|
local suffixArgs = cl.splitAtWhitespace(clangOpts)
|
|
for ai=1,#suffixArgs do
|
|
args[#args+1] = suffixArgs[ai]
|
|
end
|
|
end
|
|
compArgs[fi] = args
|
|
|
|
-- if (fi == 1) then print("Args: "..table.concat(args, ', ').."\n") end
|
|
end
|
|
end
|
|
|
|
local function GetColorizeTripleFunc(color)
|
|
return function(pre, tag, post)
|
|
return
|
|
colorize(pre, Bold..White)..
|
|
colorize(tag, Bold..color)..
|
|
colorize(post, Bold..White)
|
|
end
|
|
end
|
|
|
|
local ColorizeErrorFunc = GetColorizeTripleFunc(Red)
|
|
local ColorizeWarningFunc = GetColorizeTripleFunc(Purple)
|
|
|
|
for fi=1,#files do
|
|
local fn = files[fi]
|
|
g_curFileName = fn
|
|
|
|
local index = cl.createIndex(true, false)
|
|
local opts = useCompDb and compArgs[fi] or clangOpts or {}
|
|
|
|
do
|
|
local f, msg = io.open(fn)
|
|
if (f == nil) then
|
|
errprintf("ERROR: Failed opening %s", msg)
|
|
goto nextfile
|
|
end
|
|
f:close()
|
|
end
|
|
|
|
local tu = index:parse(fn, opts)
|
|
|
|
if (tu == nil) then
|
|
errprintf("ERROR: Failed parsing %s", fn)
|
|
goto nextfile
|
|
end
|
|
|
|
if (not quiet) then
|
|
local diags = tu:diagnostics()
|
|
for i=1,#diags do
|
|
local text = diags[i].text
|
|
if (useColors) then
|
|
text = text:gsub("(.*)(error: )(.*)", ColorizeErrorFunc)
|
|
text = text:gsub("(.*)(warning: )(.*)", ColorizeWarningFunc)
|
|
end
|
|
errprintf("%s", text)
|
|
end
|
|
end
|
|
|
|
if (not dryrun) then
|
|
local tuCursor = tu:cursor()
|
|
tuCursor:children(GetTypeVisitor)
|
|
|
|
if (g_structDecl == nil) then
|
|
if (not quiet and not useCompDb) then
|
|
-- XXX: This is kind of noisy even in non-DB
|
|
-- mode. E.g. "mgrep.lua *.c": some C files may not include a
|
|
-- particular header.
|
|
errprintf("%s: Didn't find declaration for '%s'", fn, typeName)
|
|
end
|
|
else
|
|
tuCursor:children(SearchVisitor)
|
|
printResults()
|
|
clearResults()
|
|
g_structDecl = nil
|
|
g_cursorKind = nil
|
|
end
|
|
end
|
|
|
|
::nextfile::
|
|
end
|