mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +00:00 
			
		
		
		
	Problem:  SpotBugs compiler can be further improved
Solution: Introduce event-driven primitives for SpotBugs
          (Aliaksei Budavei)
closes: vim/vim#16258
2e252474c4
Co-authored-by: Aliaksei Budavei <0x000c70@gmail.com>
		
	
		
			
				
	
	
		
			255 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			VimL
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			VimL
		
	
	
	
	
	
" Vim compiler file
 | 
						|
" Compiler:     Spotbugs (Java static checker; needs javac compiled classes)
 | 
						|
" Maintainers:  @konfekt and @zzzyxwvut
 | 
						|
" Last Change:  2024 Dec 20
 | 
						|
 | 
						|
if exists('g:current_compiler') || bufname() !~# '\.java\=$' || wordcount().chars < 9
 | 
						|
  finish
 | 
						|
endif
 | 
						|
 | 
						|
let s:cpo_save = &cpo
 | 
						|
set cpo&vim
 | 
						|
 | 
						|
" Unfortunately Spotbugs does not output absolute paths, so you need to
 | 
						|
" pass the directory of the files being checked as `-sourcepath` parameter.
 | 
						|
" The regex, auxpath and glob try to include all dependent classes of the
 | 
						|
" current buffer. See https://github.com/spotbugs/spotbugs/issues/856
 | 
						|
 | 
						|
" FIXME: When "search()" is used with the "e" flag, it makes no _further_
 | 
						|
" progress after claiming an EOL match (i.e. "\_" or "\n", but not "$").
 | 
						|
" XXX: Omit anonymous class declarations
 | 
						|
let s:keywords = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\|package\)\%(\s\|$\)'
 | 
						|
let s:type_names = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\)\s*\(\K\k*\)\>'
 | 
						|
" Capture ";" for counting a class file directory (see s:package_dir_heads below)
 | 
						|
let s:package_names = '\C\<package\s*\(\K\%(\k*\.\=\)\+;\)'
 | 
						|
let s:package = ''
 | 
						|
 | 
						|
if has('syntax') && exists('g:syntax_on') &&
 | 
						|
    \ exists('b:current_syntax') && b:current_syntax == 'java' &&
 | 
						|
    \ hlexists('javaClassDecl') && hlexists('javaExternal')
 | 
						|
 | 
						|
  function! s:GetDeclaredTypeNames() abort
 | 
						|
    if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
 | 
						|
      return [expand('%:t:r')]
 | 
						|
    endif
 | 
						|
    defer execute('silent! normal! g``')
 | 
						|
    call cursor(1, 1)
 | 
						|
    let type_names = []
 | 
						|
    let lnum = search(s:keywords, 'eW')
 | 
						|
    while lnum > 0
 | 
						|
      let name_attr = synIDattr(synID(lnum, (col('.') - 1), 0), 'name')
 | 
						|
      if name_attr ==# 'javaClassDecl'
 | 
						|
        let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:type_names)
 | 
						|
        if !empty(tokens) | call add(type_names, tokens[1]) | endif
 | 
						|
      elseif name_attr ==# 'javaExternal'
 | 
						|
        let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:package_names)
 | 
						|
        if !empty(tokens) | let s:package = tokens[1] | endif
 | 
						|
      endif
 | 
						|
      let lnum = search(s:keywords, 'eW')
 | 
						|
    endwhile
 | 
						|
    return type_names
 | 
						|
  endfunction
 | 
						|
 | 
						|
else
 | 
						|
  function! s:GetDeclaredTypeNames() abort
 | 
						|
    if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
 | 
						|
      return [expand('%:t:r')]
 | 
						|
    endif
 | 
						|
    " Undo the unsetting of &hls, see below
 | 
						|
    if &hls
 | 
						|
      defer execute('set hls')
 | 
						|
    endif
 | 
						|
    " Possibly restore the current values for registers '"' and "y", see below
 | 
						|
    defer call('setreg', ['"', getreg('"'), getregtype('"')])
 | 
						|
    defer call('setreg', ['y', getreg('y'), getregtype('y')])
 | 
						|
    defer execute('silent bwipeout')
 | 
						|
    " Copy buffer contents for modification
 | 
						|
    silent %y y
 | 
						|
    new
 | 
						|
    " Apply ":help scratch-buffer" effects and match "$" in Java (generated)
 | 
						|
    " type names (see s:type_names)
 | 
						|
    setlocal iskeyword+=$ buftype=nofile bufhidden=hide noswapfile nohls
 | 
						|
    0put y
 | 
						|
    " Discard text blocks and strings
 | 
						|
    silent keeppatterns %s/\\\@<!"""\_.\{-}\\\@<!"""\|\\"//ge
 | 
						|
    silent keeppatterns %s/".*"//ge
 | 
						|
    " Discard comments
 | 
						|
    silent keeppatterns %s/\/\/.\+$//ge
 | 
						|
    silent keeppatterns %s/\/\*\_.\{-}\*\///ge
 | 
						|
    call cursor(1, 1)
 | 
						|
    let type_names = []
 | 
						|
    let lnum = search(s:keywords, 'eW')
 | 
						|
    while lnum > 0
 | 
						|
      let line = getline(lnum)
 | 
						|
      if line =~# '\<package\>'
 | 
						|
        let tokens = matchlist(line..getline(lnum + 1), s:package_names)
 | 
						|
        if !empty(tokens) | let s:package = tokens[1] | endif
 | 
						|
      else
 | 
						|
        let tokens = matchlist(line..getline(lnum + 1), s:type_names)
 | 
						|
        if !empty(tokens) | call add(type_names, tokens[1]) | endif
 | 
						|
      endif
 | 
						|
      let lnum = search(s:keywords, 'eW')
 | 
						|
    endwhile
 | 
						|
    return type_names
 | 
						|
  endfunction
 | 
						|
endif
 | 
						|
 | 
						|
if has('win32')
 | 
						|
 | 
						|
  function! s:GlobClassFiles(src_type_name) abort
 | 
						|
    return glob(a:src_type_name..'$*.class', 1, 1)
 | 
						|
  endfunction
 | 
						|
 | 
						|
else
 | 
						|
  function! s:GlobClassFiles(src_type_name) abort
 | 
						|
    return glob(a:src_type_name..'\$*.class', 1, 1)
 | 
						|
  endfunction
 | 
						|
endif
 | 
						|
 | 
						|
if exists('b:spotbugs_properties')
 | 
						|
  " Let "ftplugin/java.vim" merge global entries, if any, in buffer-local
 | 
						|
  " entries
 | 
						|
 | 
						|
  function! s:GetProperty(name, default) abort
 | 
						|
    return get(b:spotbugs_properties, a:name, a:default)
 | 
						|
  endfunction
 | 
						|
 | 
						|
elseif exists('g:spotbugs_properties')
 | 
						|
 | 
						|
  function! s:GetProperty(name, default) abort
 | 
						|
    return get(g:spotbugs_properties, a:name, a:default)
 | 
						|
  endfunction
 | 
						|
 | 
						|
else
 | 
						|
  function! s:GetProperty(dummy, default) abort
 | 
						|
    return a:default
 | 
						|
  endfunction
 | 
						|
endif
 | 
						|
 | 
						|
if (exists('g:spotbugs_properties') || exists('b:spotbugs_properties')) &&
 | 
						|
    \ ((!empty(s:GetProperty('sourceDirPath', [])) &&
 | 
						|
        \ !empty(s:GetProperty('classDirPath', []))) ||
 | 
						|
    \ (!empty(s:GetProperty('testSourceDirPath', [])) &&
 | 
						|
        \ !empty(s:GetProperty('testClassDirPath', []))))
 | 
						|
 | 
						|
  function! s:CommonIdxsAndDirs() abort
 | 
						|
    let src_dir_path = s:GetProperty('sourceDirPath', [])
 | 
						|
    let bin_dir_path = s:GetProperty('classDirPath', [])
 | 
						|
    let test_src_dir_path = s:GetProperty('testSourceDirPath', [])
 | 
						|
    let test_bin_dir_path = s:GetProperty('testClassDirPath', [])
 | 
						|
    let dir_cnt = min([len(src_dir_path), len(bin_dir_path)])
 | 
						|
    let test_dir_cnt = min([len(test_src_dir_path), len(test_bin_dir_path)])
 | 
						|
    " Do not break up path pairs with filtering!
 | 
						|
    return [[range(dir_cnt),
 | 
						|
            \ src_dir_path[0 : dir_cnt - 1],
 | 
						|
            \ bin_dir_path[0 : dir_cnt - 1]],
 | 
						|
        \ [range(test_dir_cnt),
 | 
						|
            \ test_src_dir_path[0 : test_dir_cnt - 1],
 | 
						|
            \ test_bin_dir_path[0 : test_dir_cnt - 1]]]
 | 
						|
  endfunction
 | 
						|
 | 
						|
  let s:common_idxs_and_dirs = s:CommonIdxsAndDirs()
 | 
						|
  delfunction s:CommonIdxsAndDirs
 | 
						|
 | 
						|
  function! s:FindClassFiles(src_type_name) abort
 | 
						|
    let class_files = []
 | 
						|
    " Match pairwise the components of source and class pathnames
 | 
						|
    for [idxs, src_dirs, bin_dirs] in s:common_idxs_and_dirs
 | 
						|
      " Do not use "fnamemodify(a:src_type_name, ':p:s?src?bin?')" because
 | 
						|
      " only the rightmost "src" is looked for
 | 
						|
      for idx in idxs
 | 
						|
        let tail_idx = strridx(a:src_type_name, src_dirs[idx])
 | 
						|
        " No such directory or no such inner type (i.e. without "$")
 | 
						|
        if tail_idx < 0 | continue | endif
 | 
						|
        " Substitute "bin_dirs[idx]" for the rightmost "src_dirs[idx]"
 | 
						|
        let candidate_type_name = strpart(a:src_type_name, 0, tail_idx)..
 | 
						|
            \ bin_dirs[idx]..
 | 
						|
            \ strpart(a:src_type_name, (tail_idx + strlen(src_dirs[idx])))
 | 
						|
        for candidate in insert(s:GlobClassFiles(candidate_type_name),
 | 
						|
              \ candidate_type_name..'.class')
 | 
						|
          if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif
 | 
						|
        endfor
 | 
						|
        if !empty(class_files) | break | endif
 | 
						|
      endfor
 | 
						|
      if !empty(class_files) | break | endif
 | 
						|
    endfor
 | 
						|
    return class_files
 | 
						|
  endfunction
 | 
						|
 | 
						|
else
 | 
						|
  function! s:FindClassFiles(src_type_name) abort
 | 
						|
    let class_files = []
 | 
						|
    for candidate in insert(s:GlobClassFiles(a:src_type_name),
 | 
						|
          \ a:src_type_name..'.class')
 | 
						|
      if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif
 | 
						|
    endfor
 | 
						|
    return class_files
 | 
						|
  endfunction
 | 
						|
endif
 | 
						|
 | 
						|
if exists('g:spotbugs_alternative_path') &&
 | 
						|
    \ !empty(get(g:spotbugs_alternative_path, 'fromPath', '')) &&
 | 
						|
    \ !empty(get(g:spotbugs_alternative_path, 'toPath', ''))
 | 
						|
 | 
						|
  " See https://github.com/spotbugs/spotbugs/issues/909
 | 
						|
  function! s:ResolveAbsolutePathname() abort
 | 
						|
    let pathname = expand('%:p')
 | 
						|
    let head_idx = stridx(pathname, g:spotbugs_alternative_path.toPath)
 | 
						|
    " No such file: a mismatched path request for a project
 | 
						|
    if head_idx < 0 | return pathname | endif
 | 
						|
    " Settle for failure with file readability tests _in s:FindClassFiles()_
 | 
						|
    return strpart(pathname, 0, head_idx)..
 | 
						|
        \ g:spotbugs_alternative_path.fromPath..
 | 
						|
        \ strpart(pathname, (head_idx + strlen(g:spotbugs_alternative_path.toPath)))
 | 
						|
  endfunction
 | 
						|
 | 
						|
else
 | 
						|
  function! s:ResolveAbsolutePathname() abort
 | 
						|
    return expand('%:p')
 | 
						|
  endfunction
 | 
						|
endif
 | 
						|
 | 
						|
function! s:CollectClassFiles() abort
 | 
						|
  " Possibly obtain a symlinked path for an unsupported directory name
 | 
						|
  let pathname = s:ResolveAbsolutePathname()
 | 
						|
  " Get a platform-independent pathname prefix, cf. "expand('%:p:h')..'/'"
 | 
						|
  let tail_idx = strridx(pathname, expand('%:t'))
 | 
						|
  let src_pathname = strpart(pathname, 0, tail_idx)
 | 
						|
  let all_class_files = []
 | 
						|
  " Get all type names in the current buffer and let the filename globbing
 | 
						|
  " discover inner type names from arbitrary type names
 | 
						|
  for type_name in s:GetDeclaredTypeNames()
 | 
						|
    call extend(all_class_files, s:FindClassFiles(src_pathname..type_name))
 | 
						|
  endfor
 | 
						|
  return all_class_files
 | 
						|
endfunction
 | 
						|
 | 
						|
" Expose class files for removal etc.
 | 
						|
let b:spotbugs_class_files = s:CollectClassFiles()
 | 
						|
let s:package_dir_heads = repeat(':h', (1 + strlen(substitute(s:package, '[^.;]', '', 'g'))))
 | 
						|
let s:package_root_dir = fnamemodify(s:ResolveAbsolutePathname(), s:package_dir_heads..':S')
 | 
						|
let g:current_compiler = 'spotbugs'
 | 
						|
" CompilerSet makeprg=spotbugs
 | 
						|
let &l:makeprg = 'spotbugs'..(has('win32') ? '.bat' : '')..' '..
 | 
						|
    \ get(b:, 'spotbugs_makeprg_params', get(g:, 'spotbugs_makeprg_params', '-workHard -experimental'))..
 | 
						|
    \ ' -textui -emacs -auxclasspath '..s:package_root_dir..' -sourcepath '..s:package_root_dir..' '..
 | 
						|
    \ join(b:spotbugs_class_files, ' ')
 | 
						|
" Emacs expects doubled line numbers
 | 
						|
setlocal errorformat=%f:%l:%*[0-9]\ %m,%f:-%*[0-9]:-%*[0-9]\ %m
 | 
						|
 | 
						|
" " This compiler is meant to be used for a single buffer only
 | 
						|
" exe 'CompilerSet makeprg='..escape(&l:makeprg, ' \|"')
 | 
						|
" exe 'CompilerSet errorformat='..escape(&l:errorformat, ' \|"')
 | 
						|
 | 
						|
delfunction s:CollectClassFiles
 | 
						|
delfunction s:ResolveAbsolutePathname
 | 
						|
delfunction s:FindClassFiles
 | 
						|
delfunction s:GetProperty
 | 
						|
delfunction s:GlobClassFiles
 | 
						|
delfunction s:GetDeclaredTypeNames
 | 
						|
let &cpo = s:cpo_save
 | 
						|
unlet! s:package_root_dir s:package_dir_heads s:common_idxs_and_dirs s:package
 | 
						|
unlet! s:package_names s:type_names s:keywords s:cpo_save
 | 
						|
 | 
						|
" vim: set foldmethod=syntax shiftwidth=2 expandtab:
 |