blob: 8ed45f8ee0971b789f493d17f51c2e2cf0f639c6 [file] [log] [blame]
Konfekt65311c62024-11-28 21:06:09 +01001" Vim compiler file
2" Compiler: Spotbugs (Java static checker; needs javac compiled classes)
Aliaksei Budavei2e252472024-12-27 16:46:36 +01003" Maintainers: @konfekt and @zzzyxwvut
4" Last Change: 2024 Dec 20
Konfekt65311c62024-11-28 21:06:09 +01005
6if exists('g:current_compiler') || bufname() !~# '\.java\=$' || wordcount().chars < 9
7 finish
8endif
9
10let s:cpo_save = &cpo
11set 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
21let s:keywords = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\|package\)\%(\s\|$\)'
22let 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)
24let s:package_names = '\C\<package\s*\(\K\%(\k*\.\=\)\+;\)'
25let s:package = ''
26
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +010027if has('syntax') && exists('g:syntax_on') &&
28 \ exists('b:current_syntax') && b:current_syntax == 'java' &&
29 \ hlexists('javaClassDecl') && hlexists('javaExternal')
Konfekt65311c62024-11-28 21:06:09 +010030
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
53else
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
95endif
96
97if has('win32')
98
99 function! s:GlobClassFiles(src_type_name) abort
100 return glob(a:src_type_name..'$*.class', 1, 1)
101 endfunction
102
103else
104 function! s:GlobClassFiles(src_type_name) abort
105 return glob(a:src_type_name..'\$*.class', 1, 1)
106 endfunction
107endif
108
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100109if exists('b:spotbugs_properties')
110 " Let "ftplugin/java.vim" merge global entries, if any, in buffer-local
111 " entries
Konfekt65311c62024-11-28 21:06:09 +0100112
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100113 function! s:GetProperty(name, default) abort
114 return get(b:spotbugs_properties, a:name, a:default)
115 endfunction
116
117elseif exists('g:spotbugs_properties')
118
119 function! s:GetProperty(name, default) abort
120 return get(g:spotbugs_properties, a:name, a:default)
121 endfunction
Konfekt65311c62024-11-28 21:06:09 +0100122
123else
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100124 function! s:GetProperty(dummy, default) abort
125 return a:default
126 endfunction
127endif
128
129if (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
179else
180 function! s:FindClassFiles(src_type_name) abort
181 let class_files = []
182 for candidate in insert(s:GlobClassFiles(a:src_type_name),
Konfekt65311c62024-11-28 21:06:09 +0100183 \ a:src_type_name..'.class')
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100184 if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif
185 endfor
186 return class_files
187 endfunction
188endif
189
190if 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
206else
207 function! s:ResolveAbsolutePathname() abort
208 return expand('%:p')
209 endfunction
Konfekt65311c62024-11-28 21:06:09 +0100210endif
211
212function! s:CollectClassFiles() abort
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100213 " Possibly obtain a symlinked path for an unsupported directory name
214 let pathname = s:ResolveAbsolutePathname()
Konfekt65311c62024-11-28 21:06:09 +0100215 " Get a platform-independent pathname prefix, cf. "expand('%:p:h')..'/'"
Konfekt65311c62024-11-28 21:06:09 +0100216 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
225endfunction
226
227" Expose class files for removal etc.
228let b:spotbugs_class_files = s:CollectClassFiles()
229let s:package_dir_heads = repeat(':h', (1 + strlen(substitute(s:package, '[^.;]', '', 'g'))))
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100230let s:package_root_dir = fnamemodify(s:ResolveAbsolutePathname(), s:package_dir_heads..':S')
Konfekt65311c62024-11-28 21:06:09 +0100231let g:current_compiler = 'spotbugs'
232" CompilerSet makeprg=spotbugs
233let &l:makeprg = 'spotbugs'..(has('win32') ? '.bat' : '')..' '..
234 \ get(b:, 'spotbugs_makeprg_params', get(g:, 'spotbugs_makeprg_params', '-workHard -experimental'))..
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100235 \ ' -textui -emacs -auxclasspath '..s:package_root_dir..' -sourcepath '..s:package_root_dir..' '..
Konfekt65311c62024-11-28 21:06:09 +0100236 \ join(b:spotbugs_class_files, ' ')
237" Emacs expects doubled line numbers
238setlocal 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
244delfunction s:CollectClassFiles
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100245delfunction s:ResolveAbsolutePathname
Konfekt65311c62024-11-28 21:06:09 +0100246delfunction s:FindClassFiles
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100247delfunction s:GetProperty
Konfekt65311c62024-11-28 21:06:09 +0100248delfunction s:GlobClassFiles
249delfunction s:GetDeclaredTypeNames
250let &cpo = s:cpo_save
Aliaksei Budavei368ef5a2024-12-16 21:37:54 +0100251unlet! s:package_root_dir s:package_dir_heads s:common_idxs_and_dirs s:package
252unlet! s:package_names s:type_names s:keywords s:cpo_save
Konfekt65311c62024-11-28 21:06:09 +0100253
254" vim: set foldmethod=syntax shiftwidth=2 expandtab: