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: