patch 9.1.0935: SpotBugs compiler can be improved

Problem:  SpotBugs compiler can be improved
Solution: runtime(compiler): Improve defaults and error handling for
          SpotBugs; update test_compiler.vim (Aliaksei Budavei)

runtime(compiler): Improve defaults and error handling for SpotBugs

* Keep "spotbugs#DefaultPreCompilerTestAction()" defined but
  do not assign its Funcref to the "PreCompilerTestAction"
  key of "g:spotbugs_properties": there are no default and
  there can only be introduced arbitrary "*sourceDirPath"
  entries; therefore, this assignment is confusing at best,
  given that the function's implementation delegates to
  whatever "PreCompilerAction" is.

* Allow for the possibility of relative source pathnames
  passed as arguments to Vim for the Javac default actions,
  and the necessity to have them properly reconciled when
  the current working directory is changed.

* Do not expect users to remember or know that new source
  files ‘must be’ ":argadd"'d to be then known to the Javac
  default actions; so collect the names of Java-file buffers
  and Java-file Vim arguments; and let users providing the
  "@sources" file-lists in the "g:javac_makeprg_params"
  variable update these file-lists themselves.

* Strive to not leave behind a fire-once Syntax ":autocmd"
  for a Java buffer whenever an arbitrary pre-compile action
  errors out.

* Only attempt to run a post-compiler action in the absence
  of failures for a pre-compiler action.  Note that warnings
  and failures are treated alike (?!) by the Javac compiler,
  so when previews are tried out with "--enable-preview",
  remember about passing "-Xlint:-preview" too to also let
  SpotBugs have a go.

* Properly group conditional operators when testing for key
  entries in a user-defined variable.

* Also test whether "javaExternal" is defined when choosing
  an implementation for source-file parsing.

* Two commands are provided to toggle actions for buffer-local
  autocommands:
  - SpotBugsRemoveBufferAutocmd;
  - SpotBugsDefineBufferAutocmd.

For example, try this from "~/.vim/after/ftplugin/java.vim":
------------------------------------------------------------
if exists(':SpotBugsDefineBufferAutocmd') == 2
	SpotBugsDefineBufferAutocmd BufWritePost SigUSR1
endif
------------------------------------------------------------

And ":doautocmd java_spotbugs User" can be manually used at will.

closes: #16140

Signed-off-by: Aliaksei Budavei <0x000c70@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/autoload/spotbugs.vim b/runtime/autoload/spotbugs.vim
index 9161395..e226207 100644
--- a/runtime/autoload/spotbugs.vim
+++ b/runtime/autoload/spotbugs.vim
@@ -1,10 +1,51 @@
-" Default pre- and post-compiler actions for SpotBugs
+" Default pre- and post-compiler actions and commands for SpotBugs
 " Maintainers:  @konfekt and @zzzyxwvut
-" Last Change:  2024 Nov 27
+" Last Change:  2024 Dec 08
 
 let s:save_cpo = &cpo
 set cpo&vim
 
+" Look for the setting of "g:spotbugs#state" in "ftplugin/java.vim".
+let s:state = get(g:, 'spotbugs#state', {})
+let s:commands = get(s:state, 'commands', {})
+let s:compiler = get(s:state, 'compiler', '')
+let s:readable = filereadable($VIMRUNTIME . '/compiler/' . s:compiler . '.vim')
+
+if has_key(s:commands, 'DefaultPreCompilerCommand')
+  let g:SpotBugsPreCompilerCommand = s:commands.DefaultPreCompilerCommand
+else
+
+  function! s:DefaultPreCompilerCommand(arguments) abort
+    execute 'make ' . a:arguments
+    cc
+  endfunction
+
+  let g:SpotBugsPreCompilerCommand = function('s:DefaultPreCompilerCommand')
+endif
+
+if has_key(s:commands, 'DefaultPreCompilerTestCommand')
+  let g:SpotBugsPreCompilerTestCommand = s:commands.DefaultPreCompilerTestCommand
+else
+
+  function! s:DefaultPreCompilerTestCommand(arguments) abort
+    execute 'make ' . a:arguments
+    cc
+  endfunction
+
+  let g:SpotBugsPreCompilerTestCommand = function('s:DefaultPreCompilerTestCommand')
+endif
+
+if has_key(s:commands, 'DefaultPostCompilerCommand')
+  let g:SpotBugsPostCompilerCommand = s:commands.DefaultPostCompilerCommand
+else
+
+  function! s:DefaultPostCompilerCommand(arguments) abort
+    execute 'make ' . a:arguments
+  endfunction
+
+  let g:SpotBugsPostCompilerCommand = function('s:DefaultPostCompilerCommand')
+endif
+
 if v:version > 900
 
   function! spotbugs#DeleteClassFiles() abort
@@ -127,25 +168,21 @@
 
 function! spotbugs#DefaultPostCompilerAction() abort
   " Since v7.4.191.
-  make %:S
+  call call(g:SpotBugsPostCompilerCommand, ['%:S'])
 endfunction
 
-" Look for "spotbugs#compiler" in "ftplugin/java.vim".
-let s:compiler = exists('spotbugs#compiler') ? spotbugs#compiler : ''
-let s:readable = filereadable($VIMRUNTIME . '/compiler/' . s:compiler . '.vim')
-
 if s:readable && s:compiler ==# 'maven' && executable('mvn')
 
   function! spotbugs#DefaultPreCompilerAction() abort
     call spotbugs#DeleteClassFiles()
     compiler maven
-    make compile
+    call call(g:SpotBugsPreCompilerCommand, ['compile'])
   endfunction
 
   function! spotbugs#DefaultPreCompilerTestAction() abort
     call spotbugs#DeleteClassFiles()
     compiler maven
-    make test-compile
+    call call(g:SpotBugsPreCompilerTestCommand, ['test-compile'])
   endfunction
 
   function! spotbugs#DefaultProperties() abort
@@ -156,10 +193,10 @@
             \ function('spotbugs#DefaultPreCompilerTestAction'),
         \ 'PostCompilerAction':
             \ function('spotbugs#DefaultPostCompilerAction'),
-        \ 'sourceDirPath':      'src/main/java',
-        \ 'classDirPath':       'target/classes',
-        \ 'testSourceDirPath':  'src/test/java',
-        \ 'testClassDirPath':   'target/test-classes',
+        \ 'sourceDirPath':      ['src/main/java'],
+        \ 'classDirPath':       ['target/classes'],
+        \ 'testSourceDirPath':  ['src/test/java'],
+        \ 'testClassDirPath':   ['target/test-classes'],
         \ }
   endfunction
 
@@ -169,13 +206,13 @@
   function! spotbugs#DefaultPreCompilerAction() abort
     call spotbugs#DeleteClassFiles()
     compiler ant
-    make compile
+    call call(g:SpotBugsPreCompilerCommand, ['compile'])
   endfunction
 
   function! spotbugs#DefaultPreCompilerTestAction() abort
     call spotbugs#DeleteClassFiles()
     compiler ant
-    make compile-test
+    call call(g:SpotBugsPreCompilerTestCommand, ['compile-test'])
   endfunction
 
   function! spotbugs#DefaultProperties() abort
@@ -186,28 +223,55 @@
             \ function('spotbugs#DefaultPreCompilerTestAction'),
         \ 'PostCompilerAction':
             \ function('spotbugs#DefaultPostCompilerAction'),
-        \ 'sourceDirPath':      'src',
-        \ 'classDirPath':       'build/classes',
-        \ 'testSourceDirPath':  'test',
-        \ 'testClassDirPath':   'build/test/classes',
+        \ 'sourceDirPath':      ['src'],
+        \ 'classDirPath':       ['build/classes'],
+        \ 'testSourceDirPath':  ['test'],
+        \ 'testClassDirPath':   ['build/test/classes'],
         \ }
   endfunction
 
   unlet s:readable s:compiler
 elseif s:readable && s:compiler ==# 'javac' && executable('javac')
+  let s:filename = tempname()
 
   function! spotbugs#DefaultPreCompilerAction() abort
     call spotbugs#DeleteClassFiles()
     compiler javac
 
     if get(b:, 'javac_makeprg_params', get(g:, 'javac_makeprg_params', '')) =~ '\s@\S'
-      " Read options and filenames from @options [@sources ...].
-      make
+      " Only read options and filenames from @options [@sources ...] and do
+      " not update these files when filelists change.
+      call call(g:SpotBugsPreCompilerCommand, [''])
     else
-      " Let Javac figure out what files to compile.
-      execute 'make ' . join(map(filter(copy(v:argv),
-          \ "v:val =~# '\\.java\\=$'"),
-          \ 'shellescape(v:val)'), ' ')
+      " Collect filenames so that Javac can figure out what to compile.
+      let filelist = []
+
+      for arg_num in range(argc(-1))
+        let arg_name = argv(arg_num)
+
+        if arg_name =~# '\.java\=$'
+          call add(filelist, fnamemodify(arg_name, ':p:S'))
+        endif
+      endfor
+
+      for buf_num in range(1, bufnr('$'))
+        if !buflisted(buf_num)
+          continue
+        endif
+
+        let buf_name = bufname(buf_num)
+
+        if buf_name =~# '\.java\=$'
+          let buf_name = fnamemodify(buf_name, ':p:S')
+
+          if index(filelist, buf_name) < 0
+            call add(filelist, buf_name)
+          endif
+        endif
+      endfor
+
+      noautocmd call writefile(filelist, s:filename)
+      call call(g:SpotBugsPreCompilerCommand, [shellescape('@' . s:filename)])
     endif
   endfunction
 
@@ -219,14 +283,13 @@
     return {
         \ 'PreCompilerAction':
             \ function('spotbugs#DefaultPreCompilerAction'),
-        \ 'PreCompilerTestAction':
-            \ function('spotbugs#DefaultPreCompilerTestAction'),
         \ 'PostCompilerAction':
             \ function('spotbugs#DefaultPostCompilerAction'),
         \ }
   endfunction
 
-  unlet s:readable s:compiler
+  unlet s:readable s:compiler g:SpotBugsPreCompilerTestCommand
+  delfunction! s:DefaultPreCompilerTestCommand
 else
 
   function! spotbugs#DefaultPreCompilerAction() abort
@@ -241,10 +304,49 @@
     return {}
   endfunction
 
-  unlet s:readable
+  " XXX: Keep "s:compiler" around for "spotbugs#DefaultPreCompilerAction()",
+  " "s:DefaultPostCompilerCommand" -- "spotbugs#DefaultPostCompilerAction()".
+  unlet s:readable g:SpotBugsPreCompilerCommand g:SpotBugsPreCompilerTestCommand
+  delfunction! s:DefaultPreCompilerCommand
+  delfunction! s:DefaultPreCompilerTestCommand
 endif
 
+function! s:DefineBufferAutocmd(event, ...) abort
+  if !exists('#java_spotbugs#User')
+    return 1
+  endif
+
+  for l:event in insert(copy(a:000), a:event)
+    if l:event != 'User'
+      execute printf('silent! autocmd! java_spotbugs %s <buffer>', l:event)
+      execute printf('autocmd java_spotbugs %s <buffer> doautocmd User', l:event)
+    endif
+  endfor
+
+  return 0
+endfunction
+
+function! s:RemoveBufferAutocmd(event, ...) abort
+  if !exists('#java_spotbugs')
+    return 1
+  endif
+
+  for l:event in insert(copy(a:000), a:event)
+    if l:event != 'User'
+      execute printf('silent! autocmd! java_spotbugs %s <buffer>', l:event)
+    endif
+  endfor
+
+  return 0
+endfunction
+
+" Documented in ":help compiler-spotbugs".
+command! -bar -nargs=+ -complete=event SpotBugsDefineBufferAutocmd
+    \ call s:DefineBufferAutocmd(<f-args>)
+command! -bar -nargs=+ -complete=event SpotBugsRemoveBufferAutocmd
+    \ call s:RemoveBufferAutocmd(<f-args>)
+
 let &cpo = s:save_cpo
-unlet s:save_cpo
+unlet s:commands s:state s:save_cpo
 
 " vim: set foldmethod=syntax shiftwidth=2 expandtab:
diff --git a/runtime/compiler/spotbugs.vim b/runtime/compiler/spotbugs.vim
index 72a5084..10d164b 100644
--- a/runtime/compiler/spotbugs.vim
+++ b/runtime/compiler/spotbugs.vim
@@ -1,7 +1,7 @@
 " Vim compiler file
 " Compiler:     Spotbugs (Java static checker; needs javac compiled classes)
 " Maintainer:   @konfekt and @zzzyxwvut
-" Last Change:  2024 Nov 27
+" Last Change:  2024 Dec 14
 
 if exists('g:current_compiler') || bufname() !~# '\.java\=$' || wordcount().chars < 9
   finish
@@ -24,8 +24,9 @@
 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')
+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\=$'
@@ -105,53 +106,113 @@
   endfunction
 endif
 
-if exists('g:spotbugs_properties') &&
-    \ (has_key(g:spotbugs_properties, 'sourceDirPath') &&
-    \ has_key(g:spotbugs_properties, 'classDirPath')) ||
-    \ (has_key(g:spotbugs_properties, 'testSourceDirPath') &&
-    \ has_key(g:spotbugs_properties, 'testClassDirPath'))
+if exists('b:spotbugs_properties')
+  " Let "ftplugin/java.vim" merge global entries, if any, in buffer-local
+  " entries
 
-function! s:FindClassFiles(src_type_name) abort
-  let class_files = []
-  " Match pairwise the components of source and class pathnames
-  for [src_dir, bin_dir] in filter([
-            \ [get(g:spotbugs_properties, 'sourceDirPath', ''),
-                \ get(g:spotbugs_properties, 'classDirPath', '')],
-            \ [get(g:spotbugs_properties, 'testSourceDirPath', ''),
-                \ get(g:spotbugs_properties, 'testClassDirPath', '')]],
-        \ '!(empty(v:val[0]) || empty(v:val[1]))')
-    " Since only the rightmost "src" is sought, while there can be any number of
-    " such filenames, no "fnamemodify(a:src_type_name, ':p:s?src?bin?')" is used
-    let tail_idx = strridx(a:src_type_name, src_dir)
-    " No such directory or no such inner type (i.e. without "$")
-    if tail_idx < 0 | continue | endif
-    " Substitute "bin_dir" for the rightmost "src_dir"
-    let candidate_type_name = strpart(a:src_type_name, 0, tail_idx)..
-        \ bin_dir..
-        \ strpart(a:src_type_name, (tail_idx + strlen(src_dir)))
-    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
-  return class_files
-endfunction
+  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:FindClassFiles(src_type_name) abort
-  let class_files = []
-  for candidate in insert(s:GlobClassFiles(a:src_type_name),
+  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
+      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 pathname = expand('%:p')
   let tail_idx = strridx(pathname, expand('%:t'))
   let src_pathname = strpart(pathname, 0, tail_idx)
   let all_class_files = []
@@ -166,11 +227,12 @@
 " 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 %:p'..s:package_dir_heads..':S -sourcepath %:p'..s:package_dir_heads..':S '..
+    \ ' -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
@@ -180,10 +242,13 @@
 " 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_dir_heads s:package s:package_names s:type_names s:keywords 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:
diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt
index 6d46458..1cd075f 100644
--- a/runtime/doc/quickfix.txt
+++ b/runtime/doc/quickfix.txt
@@ -1,4 +1,4 @@
-*quickfix.txt*  For Vim version 9.1.  Last change: 2024 Nov 28
+*quickfix.txt*  For Vim version 9.1.  Last change: 2024 Dec 16
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -1349,7 +1349,7 @@
 (Therefore, `:compiler! spotbugs` is not supported.)
 
 Commonly used compiler options can be added to 'makeprg' by setting the
-"b:" or "g:spotbugs_makeprg_params" variable.  For example: >
+"b:" or "g:spotbugs_makeprg_params" variable.  For example: >vim
 
 	let b:spotbugs_makeprg_params = "-longBugCodes -effort:max -low"
 
@@ -1359,22 +1359,25 @@
 files are placed.  However, typical Java projects use distinct directories
 for source files and class files.  To make both known to SpotBugs, assign
 their paths (distinct and relative to their common root directory) to the
-following properties (using the example of a common Maven project): >
+following properties (using the example of a common Maven project): >vim
 
 	let g:spotbugs_properties = {
-		\ 'sourceDirPath':	'src/main/java',
-		\ 'classDirPath':	'target/classes',
-		\ 'testSourceDirPath':	'src/test/java',
-		\ 'testClassDirPath':	'target/test-classes',
+		\ 'sourceDirPath':	['src/main/java'],
+		\ 'classDirPath':	['target/classes'],
+		\ 'testSourceDirPath':	['src/test/java'],
+		\ 'testClassDirPath':	['target/test-classes'],
 	\ }
 
+Note that source and class path entries are expected to come in pairs: define
+both "sourceDirPath" and "classDirPath" when you are considering at least one,
+and apply the same logic to "testSourceDirPath" and "testClassDirPath".
 Note that values for the path keys describe only for SpotBugs where to look
 for files; refer to the documentation for particular compiler plugins for more
 information.
 
 The default pre- and post-compiler actions are provided for Ant, Maven, and
 Javac compiler plugins and can be selected by assigning the name of a compiler
-plugin to the "compiler" key: >
+plugin (`ant`, `maven`, or `javac`) to the "compiler" key: >vim
 
 	let g:spotbugs_properties = {
 		\ 'compiler':		'maven',
@@ -1384,7 +1387,7 @@
 the exception made for the "PreCompilerAction" and "PreCompilerTestAction"
 values: their listed |Funcref|s will obtain no-op implementations whereas the
 implicit Funcrefs of the "compiler" key will obtain the requested defaults if
-available. >
+available. >vim
 
 	let g:spotbugs_properties = {
 		\ 'PreCompilerAction':
@@ -1393,10 +1396,10 @@
 			\ function('spotbugs#DefaultPreCompilerTestAction'),
 		\ 'PostCompilerAction':
 			\ function('spotbugs#DefaultPostCompilerAction'),
-		\ 'sourceDirPath':	'src/main/java',
-		\ 'classDirPath':	'target/classes',
-		\ 'testSourceDirPath':	'src/test/java',
-		\ 'testClassDirPath':	'target/test-classes',
+		\ 'sourceDirPath':	['src/main/java'],
+		\ 'classDirPath':	['target/classes'],
+		\ 'testSourceDirPath':	['src/test/java'],
+		\ 'testClassDirPath':	['target/test-classes'],
 	\ }
 
 With default actions, the compiler of choice will attempt to rebuild the class
@@ -1404,23 +1407,42 @@
 syntax file is loaded; then, `spotbugs` will attempt to analyze the quality of
 the compilation unit of the buffer.
 
-When default actions are not suited to a desired workflow, consider writing
-arbitrary functions yourself and matching their |Funcref|s to the supported
+Vim commands proficient in 'makeprg' [0] can be composed with default actions.
+Begin by considering which of the supported keys, "DefaultPreCompilerCommand",
+"DefaultPreCompilerTestCommand", or "DefaultPostCompilerCommand", you need to
+write an implementation for, observing that each of these keys corresponds to
+a particular "*Action" key.  Follow it by defining a new function that always
+declares an only parameter of type string and puts to use a command equivalent
+of |:make|, and assigning its |Funcref| to the selected key.  For example:
+>vim
+	function! GenericPostCompilerCommand(arguments) abort
+		execute 'make ' . a:arguments
+	endfunction
+
+	let g:spotbugs_properties = {
+		\ 'DefaultPostCompilerCommand':
+			\ function('GenericPostCompilerCommand'),
+	\ }
+
+When default actions are not suited to a desired workflow, proceed by writing
+arbitrary functions yourself and matching their Funcrefs to the supported
 keys: "PreCompilerAction", "PreCompilerTestAction", and "PostCompilerAction".
 
 The next example re-implements the default pre-compiler actions for a Maven
-project and requests other default Maven settings with the "compiler" entry: >
-
+project and requests other default Maven settings with the "compiler" entry:
+>vim
 	function! MavenPreCompilerAction() abort
 		call spotbugs#DeleteClassFiles()
 		compiler maven
 		make compile
+		cc
 	endfunction
 
 	function! MavenPreCompilerTestAction() abort
 		call spotbugs#DeleteClassFiles()
 		compiler maven
 		make test-compile
+		cc
 	endfunction
 
 	let g:spotbugs_properties = {
@@ -1433,14 +1455,46 @@
 
 Note that all entered custom settings will take precedence over the matching
 default settings in "g:spotbugs_properties".
+Note that it is necessary to notify the plugin of the result of a pre-compiler
+action before further work can be undertaken.  Using |:cc| after |:make| (or
+|:ll| after |:lmake|) as the last command of an action is the supported means
+of such communication.
+
+Two commands, "SpotBugsRemoveBufferAutocmd" and "SpotBugsDefineBufferAutocmd",
+are provided to toggle actions for buffer-local autocommands.  For example, to
+also run actions on any |BufWritePost| and |SigUSR1| event, add these lines to
+`~/.vim/after/ftplugin/java.vim`: >vim
+
+	if exists(':SpotBugsDefineBufferAutocmd') == 2
+		SpotBugsDefineBufferAutocmd BufWritePost SigUSR1
+	endif
+
+Otherwise, you can turn to `:doautocmd java_spotbugs User` at any time.
 
 The "g:spotbugs_properties" variable is consulted by the Java filetype plugin
 (|ft-java-plugin|) to arrange for the described automation, and, therefore, it
 must be defined before |FileType| events can take place for the buffers loaded
 with Java source files.  It could, for example, be set in a project-local
-|vimrc| loaded by [0].
+|vimrc| loaded by [1].
 
-[0] https://github.com/MarcWeber/vim-addon-local-vimrc/
+Both "g:spotbugs_properties" and "b:spotbugs_properties" are recognized and
+must be modifiable (|:unlockvar|).  The "*Command" entries are always treated
+as global functions to be shared among all Java buffers.
+
+The SpotBugs Java library and, by extension, its distributed shell scripts do
+not support in the `-textui` mode listed pathnames with directory filenames
+that contain blank characters [2].  To work around this limitation, consider
+making a symbolic link to such a directory from a directory that does not have
+blank characters in its name and passing this information to SpotBugs: >vim
+
+	let g:spotbugs_alternative_path = {
+		\ 'fromPath':	'path/to/dir_without_blanks',
+		\ 'toPath':	'path/to/dir with blanks',
+	\ }
+
+[0] https://github.com/Konfekt/vim-compilers
+[1] https://github.com/MarcWeber/vim-addon-local-vimrc
+[2] https://github.com/spotbugs/spotbugs/issues/909
 
 GNU MAKE						*compiler-make*
 
diff --git a/runtime/ftplugin/java.vim b/runtime/ftplugin/java.vim
index 6e12fe2..0fa7733 100644
--- a/runtime/ftplugin/java.vim
+++ b/runtime/ftplugin/java.vim
@@ -3,7 +3,7 @@
 " Maintainer:		Aliaksei Budavei <0x000c70 AT gmail DOT com>
 " Former Maintainer:	Dan Sharp
 " Repository:		https://github.com/zzzyxwvut/java-vim.git
-" Last Change:		2024 Nov 24
+" Last Change:		2024 Dec 16
 "			2024 Jan 14 by Vim Project (browsefilter)
 "			2024 May 23 by Riley Bruins <ribru17@gmail.com> ('commentstring')
 
@@ -90,63 +90,155 @@
     endif
 endif
 
-" The support for pre- and post-compiler actions for SpotBugs.
-if exists("g:spotbugs_properties") && has_key(g:spotbugs_properties, 'compiler')
-    try
-	let spotbugs#compiler = g:spotbugs_properties.compiler
-	let g:spotbugs_properties = extend(
-		\ spotbugs#DefaultProperties(),
-		\ g:spotbugs_properties,
-		\ 'force')
-    catch
-	echomsg v:errmsg
-    finally
-	call remove(g:spotbugs_properties, 'compiler')
-    endtry
-endif
+"""" Support pre- and post-compiler actions for SpotBugs.
+if (!empty(get(g:, 'spotbugs_properties', {})) ||
+	\ !empty(get(b:, 'spotbugs_properties', {}))) &&
+	\ filereadable($VIMRUNTIME . '/compiler/spotbugs.vim')
 
-if exists("g:spotbugs_properties") &&
-	    \ filereadable($VIMRUNTIME . '/compiler/spotbugs.vim')
+    function! s:SpotBugsGetProperty(name, default) abort
+	return get(
+	    \ {s:spotbugs_properties_scope}spotbugs_properties,
+	    \ a:name,
+	    \ a:default)
+    endfunction
+
+    function! s:SpotBugsHasProperty(name) abort
+	return has_key(
+	    \ {s:spotbugs_properties_scope}spotbugs_properties,
+	    \ a:name)
+    endfunction
+
+    function! s:SpotBugsGetProperties() abort
+	return {s:spotbugs_properties_scope}spotbugs_properties
+    endfunction
+
+    " Work around ":bar"s and ":autocmd"s.
+    function! s:ExecuteActionOnce(cleanup_cmd, action_cmd) abort
+	try
+	    execute a:cleanup_cmd
+	finally
+	    execute a:action_cmd
+	endtry
+    endfunction
+
+    if exists("b:spotbugs_properties")
+	let s:spotbugs_properties_scope = 'b:'
+
+	" Merge global entries, if any, in buffer-local entries, favouring
+	" defined buffer-local ones.
+	call extend(
+	    \ b:spotbugs_properties,
+	    \ get(g:, 'spotbugs_properties', {}),
+	    \ 'keep')
+    elseif exists("g:spotbugs_properties")
+	let s:spotbugs_properties_scope = 'g:'
+    endif
+
+    let s:commands = {}
+
+    for s:name in ['DefaultPreCompilerCommand',
+	    \ 'DefaultPreCompilerTestCommand',
+	    \ 'DefaultPostCompilerCommand']
+	if s:SpotBugsHasProperty(s:name)
+	    let s:commands[s:name] = remove(
+		\ s:SpotBugsGetProperties(),
+		\ s:name)
+	endif
+    endfor
+
+    if s:SpotBugsHasProperty('compiler')
+	" XXX: Postpone loading the script until all state, if any, has been
+	" collected.
+	if !empty(s:commands)
+	    let g:spotbugs#state = {
+		\ 'compiler': remove(s:SpotBugsGetProperties(), 'compiler'),
+		\ 'commands': copy(s:commands),
+	    \ }
+	else
+	    let g:spotbugs#state = {
+		\ 'compiler': remove(s:SpotBugsGetProperties(), 'compiler'),
+	    \ }
+	endif
+
+	" Merge default entries in global (or buffer-local) entries, favouring
+	" defined global (or buffer-local) ones.
+	call extend(
+	    \ {s:spotbugs_properties_scope}spotbugs_properties,
+	    \ spotbugs#DefaultProperties(),
+	    \ 'keep')
+    elseif !empty(s:commands)
+	" XXX: Postpone loading the script until all state, if any, has been
+	" collected.
+	let g:spotbugs#state = {'commands': copy(s:commands)}
+    endif
+
+    unlet s:commands s:name
     let s:request = 0
 
-    if has_key(g:spotbugs_properties, 'PreCompilerAction')
-	let s:dispatcher = 'call g:spotbugs_properties.PreCompilerAction() | '
-	let s:request += 1
-    endif
-
-    if has_key(g:spotbugs_properties, 'PreCompilerTestAction')
-	let s:dispatcher = 'call g:spotbugs_properties.PreCompilerTestAction() | '
-	let s:request += 2
-    endif
-
-    if has_key(g:spotbugs_properties, 'PostCompilerAction')
+    if s:SpotBugsHasProperty('PostCompilerAction')
 	let s:request += 4
     endif
 
+    if s:SpotBugsHasProperty('PreCompilerTestAction')
+	let s:dispatcher = printf('call call(%s, [])',
+	    \ string(s:SpotBugsGetProperties().PreCompilerTestAction))
+	let s:request += 2
+    endif
+
+    if s:SpotBugsHasProperty('PreCompilerAction')
+	let s:dispatcher = printf('call call(%s, [])',
+	    \ string(s:SpotBugsGetProperties().PreCompilerAction))
+	let s:request += 1
+    endif
+
+    " Adapt the tests for "s:FindClassFiles()" from "compiler/spotbugs.vim".
     if (s:request == 3 || s:request == 7) &&
-	    \ has_key(g:spotbugs_properties, 'sourceDirPath') &&
-	    \ has_key(g:spotbugs_properties, 'testSourceDirPath')
-	function! s:DispatchAction(path_action_pairs) abort
+	    \ (!empty(s:SpotBugsGetProperty('sourceDirPath', [])) &&
+		\ !empty(s:SpotBugsGetProperty('classDirPath', [])) &&
+	    \ !empty(s:SpotBugsGetProperty('testSourceDirPath', [])) &&
+		\ !empty(s:SpotBugsGetProperty('testClassDirPath', [])))
+	function! s:DispatchAction(paths_action_pairs) abort
 	    let name = expand('%:p')
 
-	    for [path, Action] in a:path_action_pairs
-		if name =~# (path . '.\{-}\.java\=$')
-		    call Action()
-		    break
-		endif
+	    for [paths, Action] in a:paths_action_pairs
+		for path in paths
+		    if name =~# (path . '.\{-}\.java\=$')
+			call Action()
+			return
+		    endif
+		endfor
 	    endfor
 	endfunction
 
-	let s:dispatcher = printf('call s:DispatchAction(%s) | ',
-		\ string([[g:spotbugs_properties.sourceDirPath,
-			    \ g:spotbugs_properties.PreCompilerAction],
-			\ [g:spotbugs_properties.testSourceDirPath,
-			    \ g:spotbugs_properties.PreCompilerTestAction]]))
+	let s:dir_cnt = min([
+	    \ len(s:SpotBugsGetProperties().sourceDirPath),
+	    \ len(s:SpotBugsGetProperties().classDirPath)])
+	let s:test_dir_cnt = min([
+	    \ len(s:SpotBugsGetProperties().testSourceDirPath),
+	    \ len(s:SpotBugsGetProperties().testClassDirPath)])
+
+	" Do not break up path pairs with filtering!
+	let s:dispatcher = printf('call s:DispatchAction(%s)',
+	    \ string([[s:SpotBugsGetProperties().sourceDirPath[0 : s:dir_cnt - 1],
+			\ s:SpotBugsGetProperties().PreCompilerAction],
+		\ [s:SpotBugsGetProperties().testSourceDirPath[0 : s:test_dir_cnt - 1],
+			\ s:SpotBugsGetProperties().PreCompilerTestAction]]))
+	unlet s:test_dir_cnt s:dir_cnt
+    endif
+
+    if exists("s:dispatcher")
+	function! s:ExecuteActions(pre_action, post_action) abort
+	    try
+		execute a:pre_action
+	    catch /\<E42:/
+		execute a:post_action
+	    endtry
+	endfunction
     endif
 
     if s:request
-	if exists("b:spotbugs_syntax_once")
-	    let s:actions = [{'event': 'BufWritePost'}]
+	if exists("b:spotbugs_syntax_once") || empty(join(getline(1, 8), ''))
+	    let s:actions = [{'event': 'User'}]
 	else
 	    " XXX: Handle multiple FileType events when vimrc contains more
 	    " than one filetype setting for the language, e.g.:
@@ -157,20 +249,25 @@
 	    let s:actions = [{
 		    \ 'event': 'Syntax',
 		    \ 'once': 1,
-		    \ }, {
-		    \ 'event': 'BufWritePost',
-		    \ }]
+		\ }, {
+		    \ 'event': 'User',
+		\ }]
 	endif
 
 	for s:idx in range(len(s:actions))
 	    if s:request == 7 || s:request == 6 || s:request == 5
-		let s:actions[s:idx].cmd = s:dispatcher . 'compiler spotbugs | ' .
-			\ 'call g:spotbugs_properties.PostCompilerAction()'
+		let s:actions[s:idx].cmd = printf('call s:ExecuteActions(%s, %s)',
+		    \ string(s:dispatcher),
+		    \ string(printf('compiler spotbugs | call call(%s, [])',
+			\ string(s:SpotBugsGetProperties().PostCompilerAction))))
 	    elseif s:request == 4
-		let s:actions[s:idx].cmd = 'compiler spotbugs | ' .
-			\ 'call g:spotbugs_properties.PostCompilerAction()'
+		let s:actions[s:idx].cmd = printf(
+		    \ 'compiler spotbugs | call call(%s, [])',
+		    \ string(s:SpotBugsGetProperties().PostCompilerAction))
 	    elseif s:request == 3 || s:request == 2 || s:request == 1
-		let s:actions[s:idx].cmd = s:dispatcher . 'compiler spotbugs'
+		let s:actions[s:idx].cmd = printf('call s:ExecuteActions(%s, %s)',
+		    \ string(s:dispatcher),
+		    \ string('compiler spotbugs'))
 	    else
 		let s:actions[s:idx].cmd = ''
 	    endif
@@ -182,30 +279,39 @@
 	endif
 
 	" The events are defined in s:actions.
-	silent! autocmd! java_spotbugs BufWritePost <buffer>
+	silent! autocmd! java_spotbugs User <buffer>
 	silent! autocmd! java_spotbugs Syntax <buffer>
 
 	for s:action in s:actions
-	    execute printf('autocmd java_spotbugs %s <buffer> %s',
+	    if has_key(s:action, 'once')
+		execute printf('autocmd java_spotbugs %s <buffer> ' .
+			\ 'call s:ExecuteActionOnce(%s, %s)',
 		    \ s:action.event,
-		    \ s:action.cmd . (has_key(s:action, 'once')
-			    \ ? printf(' | autocmd! java_spotbugs %s <buffer>',
-				    \ s:action.event)
-			    \ : ''))
+		    \ string(printf('autocmd! java_spotbugs %s <buffer>',
+			\ s:action.event)),
+		    \ string(s:action.cmd))
+	    else
+		execute printf('autocmd java_spotbugs %s <buffer> %s',
+		    \ s:action.event,
+		    \ s:action.cmd)
+	    endif
 	endfor
 
 	unlet! s:action s:actions s:idx s:dispatcher
     endif
 
-    unlet s:request
+    delfunction s:SpotBugsGetProperties
+    delfunction s:SpotBugsHasProperty
+    delfunction s:SpotBugsGetProperty
+    unlet! s:request s:spotbugs_properties_scope
 endif
 
 function! JavaFileTypeCleanUp() abort
     setlocal suffixes< suffixesadd< formatoptions< comments< commentstring< path< includeexpr<
     unlet! b:browsefilter
 
-    " The concatenated removals may be misparsed as a BufWritePost autocmd.
-    silent! autocmd! java_spotbugs BufWritePost <buffer>
+    " The concatenated removals may be misparsed as a User autocmd.
+    silent! autocmd! java_spotbugs User <buffer>
     silent! autocmd! java_spotbugs Syntax <buffer>
 endfunction
 
@@ -232,14 +338,16 @@
 endif
 
 if exists("*s:DispatchAction")
-    def! s:DispatchAction(path_action_pairs: list<list<any>>)
+    def! s:DispatchAction(paths_action_pairs: list<list<any>>)
 	const name: string = expand('%:p')
 
-	for [path: string, Action: func: any] in path_action_pairs
-	    if name =~# (path .. '.\{-}\.java\=$')
-		Action()
-		break
-	    endif
+	for [paths: list<string>, Action: func: any] in paths_action_pairs
+	    for path in paths
+		if name =~# (path .. '.\{-}\.java\=$')
+		    Action()
+		    return
+		endif
+	    endfor
 	endfor
     enddef
 endif