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