neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

spotbugs.vim (9919B)


      1 " Vim compiler file
      2 " Compiler:     Spotbugs (Java static checker; needs javac compiled classes)
      3 " Maintainers:  @konfekt and @zzzyxwvut
      4 " Last Change:  2024 Dec 20
      5 
      6 if exists('g:current_compiler') || bufname() !~# '\.java\=$' || wordcount().chars < 9
      7  finish
      8 endif
      9 
     10 let s:cpo_save = &cpo
     11 set cpo&vim
     12 
     13 " Unfortunately Spotbugs does not output absolute paths, so you need to
     14 " pass the directory of the files being checked as `-sourcepath` parameter.
     15 " The regex, auxpath and glob try to include all dependent classes of the
     16 " current buffer. See https://github.com/spotbugs/spotbugs/issues/856
     17 
     18 " FIXME: When "search()" is used with the "e" flag, it makes no _further_
     19 " progress after claiming an EOL match (i.e. "\_" or "\n", but not "$").
     20 " XXX: Omit anonymous class declarations
     21 let s:keywords = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\|package\)\%(\s\|$\)'
     22 let s:type_names = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\)\s*\(\K\k*\)\>'
     23 " Capture ";" for counting a class file directory (see s:package_dir_heads below)
     24 let s:package_names = '\C\<package\s*\(\K\%(\k*\.\=\)\+;\)'
     25 let s:package = ''
     26 
     27 if has('syntax') && exists('g:syntax_on') &&
     28    \ exists('b:current_syntax') && b:current_syntax == 'java' &&
     29    \ hlexists('javaClassDecl') && hlexists('javaExternal')
     30 
     31  function! s:GetDeclaredTypeNames() abort
     32    if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
     33      return [expand('%:t:r')]
     34    endif
     35    defer execute('silent! normal! g``')
     36    call cursor(1, 1)
     37    let type_names = []
     38    let lnum = search(s:keywords, 'eW')
     39    while lnum > 0
     40      let name_attr = synIDattr(synID(lnum, (col('.') - 1), 0), 'name')
     41      if name_attr ==# 'javaClassDecl'
     42        let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:type_names)
     43        if !empty(tokens) | call add(type_names, tokens[1]) | endif
     44      elseif name_attr ==# 'javaExternal'
     45        let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:package_names)
     46        if !empty(tokens) | let s:package = tokens[1] | endif
     47      endif
     48      let lnum = search(s:keywords, 'eW')
     49    endwhile
     50    return type_names
     51  endfunction
     52 
     53 else
     54  function! s:GetDeclaredTypeNames() abort
     55    if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
     56      return [expand('%:t:r')]
     57    endif
     58    " Undo the unsetting of &hls, see below
     59    if &hls
     60      defer execute('set hls')
     61    endif
     62    " Possibly restore the current values for registers '"' and "y", see below
     63    defer call('setreg', ['"', getreg('"'), getregtype('"')])
     64    defer call('setreg', ['y', getreg('y'), getregtype('y')])
     65    defer execute('silent bwipeout')
     66    " Copy buffer contents for modification
     67    silent %y y
     68    new
     69    " Apply ":help scratch-buffer" effects and match "$" in Java (generated)
     70    " type names (see s:type_names)
     71    setlocal iskeyword+=$ buftype=nofile bufhidden=hide noswapfile nohls
     72    0put y
     73    " Discard text blocks and strings
     74    silent keeppatterns %s/\\\@<!"""\_.\{-}\\\@<!"""\|\\"//ge
     75    silent keeppatterns %s/".*"//ge
     76    " Discard comments
     77    silent keeppatterns %s/\/\/.\+$//ge
     78    silent keeppatterns %s/\/\*\_.\{-}\*\///ge
     79    call cursor(1, 1)
     80    let type_names = []
     81    let lnum = search(s:keywords, 'eW')
     82    while lnum > 0
     83      let line = getline(lnum)
     84      if line =~# '\<package\>'
     85        let tokens = matchlist(line..getline(lnum + 1), s:package_names)
     86        if !empty(tokens) | let s:package = tokens[1] | endif
     87      else
     88        let tokens = matchlist(line..getline(lnum + 1), s:type_names)
     89        if !empty(tokens) | call add(type_names, tokens[1]) | endif
     90      endif
     91      let lnum = search(s:keywords, 'eW')
     92    endwhile
     93    return type_names
     94  endfunction
     95 endif
     96 
     97 if has('win32')
     98 
     99  function! s:GlobClassFiles(src_type_name) abort
    100    return glob(a:src_type_name..'$*.class', 1, 1)
    101  endfunction
    102 
    103 else
    104  function! s:GlobClassFiles(src_type_name) abort
    105    return glob(a:src_type_name..'\$*.class', 1, 1)
    106  endfunction
    107 endif
    108 
    109 if exists('b:spotbugs_properties')
    110  " Let "ftplugin/java.vim" merge global entries, if any, in buffer-local
    111  " entries
    112 
    113  function! s:GetProperty(name, default) abort
    114    return get(b:spotbugs_properties, a:name, a:default)
    115  endfunction
    116 
    117 elseif exists('g:spotbugs_properties')
    118 
    119  function! s:GetProperty(name, default) abort
    120    return get(g:spotbugs_properties, a:name, a:default)
    121  endfunction
    122 
    123 else
    124  function! s:GetProperty(dummy, default) abort
    125    return a:default
    126  endfunction
    127 endif
    128 
    129 if (exists('g:spotbugs_properties') || exists('b:spotbugs_properties')) &&
    130    \ ((!empty(s:GetProperty('sourceDirPath', [])) &&
    131        \ !empty(s:GetProperty('classDirPath', []))) ||
    132    \ (!empty(s:GetProperty('testSourceDirPath', [])) &&
    133        \ !empty(s:GetProperty('testClassDirPath', []))))
    134 
    135  function! s:CommonIdxsAndDirs() abort
    136    let src_dir_path = s:GetProperty('sourceDirPath', [])
    137    let bin_dir_path = s:GetProperty('classDirPath', [])
    138    let test_src_dir_path = s:GetProperty('testSourceDirPath', [])
    139    let test_bin_dir_path = s:GetProperty('testClassDirPath', [])
    140    let dir_cnt = min([len(src_dir_path), len(bin_dir_path)])
    141    let test_dir_cnt = min([len(test_src_dir_path), len(test_bin_dir_path)])
    142    " Do not break up path pairs with filtering!
    143    return [[range(dir_cnt),
    144            \ src_dir_path[0 : dir_cnt - 1],
    145            \ bin_dir_path[0 : dir_cnt - 1]],
    146        \ [range(test_dir_cnt),
    147            \ test_src_dir_path[0 : test_dir_cnt - 1],
    148            \ test_bin_dir_path[0 : test_dir_cnt - 1]]]
    149  endfunction
    150 
    151  let s:common_idxs_and_dirs = s:CommonIdxsAndDirs()
    152  delfunction s:CommonIdxsAndDirs
    153 
    154  function! s:FindClassFiles(src_type_name) abort
    155    let class_files = []
    156    " Match pairwise the components of source and class pathnames
    157    for [idxs, src_dirs, bin_dirs] in s:common_idxs_and_dirs
    158      " Do not use "fnamemodify(a:src_type_name, ':p:s?src?bin?')" because
    159      " only the rightmost "src" is looked for
    160      for idx in idxs
    161        let tail_idx = strridx(a:src_type_name, src_dirs[idx])
    162        " No such directory or no such inner type (i.e. without "$")
    163        if tail_idx < 0 | continue | endif
    164        " Substitute "bin_dirs[idx]" for the rightmost "src_dirs[idx]"
    165        let candidate_type_name = strpart(a:src_type_name, 0, tail_idx)..
    166            \ bin_dirs[idx]..
    167            \ strpart(a:src_type_name, (tail_idx + strlen(src_dirs[idx])))
    168        for candidate in insert(s:GlobClassFiles(candidate_type_name),
    169              \ candidate_type_name..'.class')
    170          if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif
    171        endfor
    172        if !empty(class_files) | break | endif
    173      endfor
    174      if !empty(class_files) | break | endif
    175    endfor
    176    return class_files
    177  endfunction
    178 
    179 else
    180  function! s:FindClassFiles(src_type_name) abort
    181    let class_files = []
    182    for candidate in insert(s:GlobClassFiles(a:src_type_name),
    183          \ a:src_type_name..'.class')
    184      if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif
    185    endfor
    186    return class_files
    187  endfunction
    188 endif
    189 
    190 if exists('g:spotbugs_alternative_path') &&
    191    \ !empty(get(g:spotbugs_alternative_path, 'fromPath', '')) &&
    192    \ !empty(get(g:spotbugs_alternative_path, 'toPath', ''))
    193 
    194  " See https://github.com/spotbugs/spotbugs/issues/909
    195  function! s:ResolveAbsolutePathname() abort
    196    let pathname = expand('%:p')
    197    let head_idx = stridx(pathname, g:spotbugs_alternative_path.toPath)
    198    " No such file: a mismatched path request for a project
    199    if head_idx < 0 | return pathname | endif
    200    " Settle for failure with file readability tests _in s:FindClassFiles()_
    201    return strpart(pathname, 0, head_idx)..
    202        \ g:spotbugs_alternative_path.fromPath..
    203        \ strpart(pathname, (head_idx + strlen(g:spotbugs_alternative_path.toPath)))
    204  endfunction
    205 
    206 else
    207  function! s:ResolveAbsolutePathname() abort
    208    return expand('%:p')
    209  endfunction
    210 endif
    211 
    212 function! s:CollectClassFiles() abort
    213  " Possibly obtain a symlinked path for an unsupported directory name
    214  let pathname = s:ResolveAbsolutePathname()
    215  " Get a platform-independent pathname prefix, cf. "expand('%:p:h')..'/'"
    216  let tail_idx = strridx(pathname, expand('%:t'))
    217  let src_pathname = strpart(pathname, 0, tail_idx)
    218  let all_class_files = []
    219  " Get all type names in the current buffer and let the filename globbing
    220  " discover inner type names from arbitrary type names
    221  for type_name in s:GetDeclaredTypeNames()
    222    call extend(all_class_files, s:FindClassFiles(src_pathname..type_name))
    223  endfor
    224  return all_class_files
    225 endfunction
    226 
    227 " Expose class files for removal etc.
    228 let b:spotbugs_class_files = s:CollectClassFiles()
    229 let s:package_dir_heads = repeat(':h', (1 + strlen(substitute(s:package, '[^.;]', '', 'g'))))
    230 let s:package_root_dir = fnamemodify(s:ResolveAbsolutePathname(), s:package_dir_heads..':S')
    231 let g:current_compiler = 'spotbugs'
    232 " CompilerSet makeprg=spotbugs
    233 let &l:makeprg = 'spotbugs'..(has('win32') ? '.bat' : '')..' '..
    234    \ get(b:, 'spotbugs_makeprg_params', get(g:, 'spotbugs_makeprg_params', '-workHard -experimental'))..
    235    \ ' -textui -emacs -auxclasspath '..s:package_root_dir..' -sourcepath '..s:package_root_dir..' '..
    236    \ join(b:spotbugs_class_files, ' ')
    237 " Emacs expects doubled line numbers
    238 setlocal errorformat=%f:%l:%*[0-9]\ %m,%f:-%*[0-9]:-%*[0-9]\ %m
    239 
    240 " " This compiler is meant to be used for a single buffer only
    241 " exe 'CompilerSet makeprg='..escape(&l:makeprg, ' \|"')
    242 " exe 'CompilerSet errorformat='..escape(&l:errorformat, ' \|"')
    243 
    244 delfunction s:CollectClassFiles
    245 delfunction s:ResolveAbsolutePathname
    246 delfunction s:FindClassFiles
    247 delfunction s:GetProperty
    248 delfunction s:GlobClassFiles
    249 delfunction s:GetDeclaredTypeNames
    250 let &cpo = s:cpo_save
    251 unlet! s:package_root_dir s:package_dir_heads s:common_idxs_and_dirs s:package
    252 unlet! s:package_names s:type_names s:keywords s:cpo_save
    253 
    254 " vim: set foldmethod=syntax shiftwidth=2 expandtab: