blob: 72a508497658a7d55275b323f36327fb8c9f584a [file] [log] [blame]
Konfekt65311c62024-11-28 21:06:09 +01001" Vim compiler file
2" Compiler: Spotbugs (Java static checker; needs javac compiled classes)
3" Maintainer: @konfekt and @zzzyxwvut
4" Last Change: 2024 Nov 27
5
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
27if has('syntax') && exists('g:syntax_on') && exists('b:current_syntax') &&
28 \ b:current_syntax == 'java' && hlexists('javaClassDecl')
29
30 function! s:GetDeclaredTypeNames() abort
31 if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
32 return [expand('%:t:r')]
33 endif
34 defer execute('silent! normal! g``')
35 call cursor(1, 1)
36 let type_names = []
37 let lnum = search(s:keywords, 'eW')
38 while lnum > 0
39 let name_attr = synIDattr(synID(lnum, (col('.') - 1), 0), 'name')
40 if name_attr ==# 'javaClassDecl'
41 let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:type_names)
42 if !empty(tokens) | call add(type_names, tokens[1]) | endif
43 elseif name_attr ==# 'javaExternal'
44 let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:package_names)
45 if !empty(tokens) | let s:package = tokens[1] | endif
46 endif
47 let lnum = search(s:keywords, 'eW')
48 endwhile
49 return type_names
50 endfunction
51
52else
53 function! s:GetDeclaredTypeNames() abort
54 if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
55 return [expand('%:t:r')]
56 endif
57 " Undo the unsetting of &hls, see below
58 if &hls
59 defer execute('set hls')
60 endif
61 " Possibly restore the current values for registers '"' and "y", see below
62 defer call('setreg', ['"', getreg('"'), getregtype('"')])
63 defer call('setreg', ['y', getreg('y'), getregtype('y')])
64 defer execute('silent bwipeout')
65 " Copy buffer contents for modification
66 silent %y y
67 new
68 " Apply ":help scratch-buffer" effects and match "$" in Java (generated)
69 " type names (see s:type_names)
70 setlocal iskeyword+=$ buftype=nofile bufhidden=hide noswapfile nohls
71 0put y
72 " Discard text blocks and strings
73 silent keeppatterns %s/\\\@<!"""\_.\{-}\\\@<!"""\|\\"//ge
74 silent keeppatterns %s/".*"//ge
75 " Discard comments
76 silent keeppatterns %s/\/\/.\+$//ge
77 silent keeppatterns %s/\/\*\_.\{-}\*\///ge
78 call cursor(1, 1)
79 let type_names = []
80 let lnum = search(s:keywords, 'eW')
81 while lnum > 0
82 let line = getline(lnum)
83 if line =~# '\<package\>'
84 let tokens = matchlist(line..getline(lnum + 1), s:package_names)
85 if !empty(tokens) | let s:package = tokens[1] | endif
86 else
87 let tokens = matchlist(line..getline(lnum + 1), s:type_names)
88 if !empty(tokens) | call add(type_names, tokens[1]) | endif
89 endif
90 let lnum = search(s:keywords, 'eW')
91 endwhile
92 return type_names
93 endfunction
94endif
95
96if has('win32')
97
98 function! s:GlobClassFiles(src_type_name) abort
99 return glob(a:src_type_name..'$*.class', 1, 1)
100 endfunction
101
102else
103 function! s:GlobClassFiles(src_type_name) abort
104 return glob(a:src_type_name..'\$*.class', 1, 1)
105 endfunction
106endif
107
108if exists('g:spotbugs_properties') &&
109 \ (has_key(g:spotbugs_properties, 'sourceDirPath') &&
110 \ has_key(g:spotbugs_properties, 'classDirPath')) ||
111 \ (has_key(g:spotbugs_properties, 'testSourceDirPath') &&
112 \ has_key(g:spotbugs_properties, 'testClassDirPath'))
113
114function! s:FindClassFiles(src_type_name) abort
115 let class_files = []
116 " Match pairwise the components of source and class pathnames
117 for [src_dir, bin_dir] in filter([
118 \ [get(g:spotbugs_properties, 'sourceDirPath', ''),
119 \ get(g:spotbugs_properties, 'classDirPath', '')],
120 \ [get(g:spotbugs_properties, 'testSourceDirPath', ''),
121 \ get(g:spotbugs_properties, 'testClassDirPath', '')]],
122 \ '!(empty(v:val[0]) || empty(v:val[1]))')
123 " Since only the rightmost "src" is sought, while there can be any number of
124 " such filenames, no "fnamemodify(a:src_type_name, ':p:s?src?bin?')" is used
125 let tail_idx = strridx(a:src_type_name, src_dir)
126 " No such directory or no such inner type (i.e. without "$")
127 if tail_idx < 0 | continue | endif
128 " Substitute "bin_dir" for the rightmost "src_dir"
129 let candidate_type_name = strpart(a:src_type_name, 0, tail_idx)..
130 \ bin_dir..
131 \ strpart(a:src_type_name, (tail_idx + strlen(src_dir)))
132 for candidate in insert(s:GlobClassFiles(candidate_type_name),
133 \ candidate_type_name..'.class')
134 if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif
135 endfor
136 if !empty(class_files) | break | endif
137 endfor
138 return class_files
139endfunction
140
141else
142function! s:FindClassFiles(src_type_name) abort
143 let class_files = []
144 for candidate in insert(s:GlobClassFiles(a:src_type_name),
145 \ a:src_type_name..'.class')
146 if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif
147 endfor
148 return class_files
149endfunction
150endif
151
152function! s:CollectClassFiles() abort
153 " Get a platform-independent pathname prefix, cf. "expand('%:p:h')..'/'"
154 let pathname = expand('%:p')
155 let tail_idx = strridx(pathname, expand('%:t'))
156 let src_pathname = strpart(pathname, 0, tail_idx)
157 let all_class_files = []
158 " Get all type names in the current buffer and let the filename globbing
159 " discover inner type names from arbitrary type names
160 for type_name in s:GetDeclaredTypeNames()
161 call extend(all_class_files, s:FindClassFiles(src_pathname..type_name))
162 endfor
163 return all_class_files
164endfunction
165
166" Expose class files for removal etc.
167let b:spotbugs_class_files = s:CollectClassFiles()
168let s:package_dir_heads = repeat(':h', (1 + strlen(substitute(s:package, '[^.;]', '', 'g'))))
169let g:current_compiler = 'spotbugs'
170" CompilerSet makeprg=spotbugs
171let &l:makeprg = 'spotbugs'..(has('win32') ? '.bat' : '')..' '..
172 \ get(b:, 'spotbugs_makeprg_params', get(g:, 'spotbugs_makeprg_params', '-workHard -experimental'))..
173 \ ' -textui -emacs -auxclasspath %:p'..s:package_dir_heads..':S -sourcepath %:p'..s:package_dir_heads..':S '..
174 \ join(b:spotbugs_class_files, ' ')
175" Emacs expects doubled line numbers
176setlocal errorformat=%f:%l:%*[0-9]\ %m,%f:-%*[0-9]:-%*[0-9]\ %m
177
178" " This compiler is meant to be used for a single buffer only
179" exe 'CompilerSet makeprg='..escape(&l:makeprg, ' \|"')
180" exe 'CompilerSet errorformat='..escape(&l:errorformat, ' \|"')
181
182delfunction s:CollectClassFiles
183delfunction s:FindClassFiles
184delfunction s:GlobClassFiles
185delfunction s:GetDeclaredTypeNames
186let &cpo = s:cpo_save
187unlet s:package_dir_heads s:package s:package_names s:type_names s:keywords s:cpo_save
188
189" vim: set foldmethod=syntax shiftwidth=2 expandtab: