In a rather large code base with a few layers is there a way in vim or from the command line to find all classes that are derived from a base class? grep is an option but can be slow since grep does not index.
Neither cscope nor ctags allow us to deal with inheritance directly but it's relatively easy to work around that limitation because derived classes are also indexed.
cscope
In cscope, looking for "C symbol" Foobar
usually lists the original class and classes inheriting from it. Since the search is done against a database, it is lightning fast.
Alternatively, you could use cscope's egrep searching capabilities with a pattern like :.*Foobar
to list only classes inheriting from Foobar
.
So, even if we don't have a dedicated "Find classes inheriting from this class" command, we can get the work done without much effort.
ctags
While ctags allows you to include inheritance information with --fields=+i
, that information can't be used directly in Vim. The inherits
field is parsed by Vim, though, so it might be possible to build a quick and dirty solution using taglist()
.
ack, ag
Those two programs work more or less like grep but they are targeted toward searching in source code so they are really faster than grep.
In my Vim config, :grep
is set to run the ag
program instead of the default grep
so, searching for classes derived from the class under the cursor would look like:
:grep :.*<C-r><C-w><CR>
Here are the relevant lines from my ~/.vimrc
:
if executable("ag")
set grepprg=ag\ --nogroup\ --nocolor\ --ignore-case\ --column
set grepformat=%f:%l:%c:%m,%f:%l:%m
endif
If you build your tags files with Exuberant CTags using inheritance information (see the --fields option), then the following script will work. It adds an :Inherits
command which takes either the name of a class (e.g. :Inherits Foo
) or a regular expression.
Like the :tag
command, you indicate that you want the search with a regex by preceding it with a '\' character, e.g. :Inherits \Foo.*
.
The results are put into the window's location list, which you browse with :ll
, :lne
, :lp
, etc. VIM doesn't seem to allow scripts to modify the tag list which is what I'd prefer.
If you're wondering why I don't use taglist()
for this, it's because taglist()
is incredibly slow on large tag files. The original post had a version using taglist()
, if you're curious you can browse the edit history.
" Parse an Exuberant Ctags record using the same format as taglist()
"
" Throws CtagsParseErr if there is a general problem parsing the record
function! ParseCtagsRec(record, tag_dir)
let tag = {}
" Parse the standard fields
let sep_pos = stridx(a:record, "\t")
if sep_pos < 1
throw 'CtagsParseErr'
endif
let tag['name'] = a:record[:sep_pos - 1]
let tail = a:record[sep_pos + 1:]
let sep_pos = stridx(tail, "\t")
if sep_pos < 1
throw 'CtagsParseErr'
endif
" '/' will work as a path separator on most OS's, but there
" should really be an OS independent way to build paths.
let tag['filename'] = a:tag_dir.'/'.tail[:sep_pos - 1]
let tail = tail[sep_pos + 1:]
let sep_pos = stridx(tail, ";\"\t")
if sep_pos < 1
throw 'CtagsParseErr'
endif
let tag['cmd'] = tail[:sep_pos - 1]
" Parse the Exuberant Ctags extension fields
let extensions = tail[sep_pos + 3:]
for extension in split(extensions, '\t')
let sep_pos = stridx(extension, ':')
if sep_pos < 1
if has_key(tag, 'kind')
throw 'CtagsParseErr'
endif
let tag['kind'] = extension
else
let tag[extension[:sep_pos - 1]] = extension[sep_pos + 1:]
endif
endfor
return tag
endfunction
" Find all classes derived from a given class, or a regex (preceded by a '/')
" The results are placed in the current windows location list.
function! Inherits(cls_or_regex)
if a:cls_or_regex[0] == '/'
let regex = a:cls_or_regex[1:]
else
let regex = '\<'.a:cls_or_regex.'\>$'
endif
let loc_list = []
let tfiles = tagfiles()
let tag_count = 0
let found_count = 0
for file in tfiles
let tag_dir = fnamemodify(file, ':p:h')
try
for line in readfile(file)
let tag_count += 1
if tag_count % 10000 == 0
echo tag_count 'tags scanned,' found_count 'matching classes found. Still searching...'
redraw
endif
if line[0] == '!'
continue
endif
let tag = ParseCtagsRec(line, tag_dir)
if has_key(tag, 'inherits')
let baselist = split(tag['inherits'], ',\s*')
for base in baselist
if match(base, regex) != -1
let location = {}
let location['filename'] = tag['filename']
let cmd = tag['cmd']
if cmd[0] == '/' || cmd[0] == '?'
let location['pattern'] = cmd[1:-2]
else
let location['lnum'] = str2nr(cmd)
endif
call add(loc_list, location)
let found_count += 1
endif
endfor
endif
endfor
catch /^OptionErr$/
echo 'Parsing error: Failed to parse an option.'
return
catch /^CtagsParseErr$/
echo 'Parsing error: Tags files does not appear to be an Exuberant Ctags file.'
return
catch
echo 'Could not read tag file:' file
return
endtry
endfor
call setloclist(0, loc_list)
echo tag_count 'tags scanned,' found_count 'matching classes found.'
endfunction
command! -nargs=1 -complete=tag Inherits call Inherits('<args>')
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With