Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 1 | " Vim compiler file |
| 2 | " Compiler: Spotbugs (Java static checker; needs javac compiled classes) |
| 3 | " Maintainer: @konfekt and @zzzyxwvut |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 4 | " Last Change: 2024 Dec 14 |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 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 | |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 27 | if has('syntax') && exists('g:syntax_on') && |
| 28 | \ exists('b:current_syntax') && b:current_syntax == 'java' && |
| 29 | \ hlexists('javaClassDecl') && hlexists('javaExternal') |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 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 | |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 109 | if exists('b:spotbugs_properties') |
| 110 | " Let "ftplugin/java.vim" merge global entries, if any, in buffer-local |
| 111 | " entries |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 112 | |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 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 |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 122 | |
| 123 | else |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 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), |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 183 | \ a:src_type_name..'.class') |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 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 |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 210 | endif |
| 211 | |
| 212 | function! s:CollectClassFiles() abort |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 213 | " Possibly obtain a symlinked path for an unsupported directory name |
| 214 | let pathname = s:ResolveAbsolutePathname() |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 215 | " Get a platform-independent pathname prefix, cf. "expand('%:p:h')..'/'" |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 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')))) |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 230 | let s:package_root_dir = fnamemodify(s:ResolveAbsolutePathname(), s:package_dir_heads..':S') |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 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')).. |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 235 | \ ' -textui -emacs -auxclasspath '..s:package_root_dir..' -sourcepath '..s:package_root_dir..' '.. |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 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 |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 245 | delfunction s:ResolveAbsolutePathname |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 246 | delfunction s:FindClassFiles |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 247 | delfunction s:GetProperty |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 248 | delfunction s:GlobClassFiles |
| 249 | delfunction s:GetDeclaredTypeNames |
| 250 | let &cpo = s:cpo_save |
Aliaksei Budavei | 368ef5a | 2024-12-16 21:37:54 +0100 | [diff] [blame] | 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 |
Konfekt | 65311c6 | 2024-11-28 21:06:09 +0100 | [diff] [blame] | 253 | |
| 254 | " vim: set foldmethod=syntax shiftwidth=2 expandtab: |