runtime(compiler): include spotbugs Java linter

closes: #16001

Co-authored-by: Aliaksei Budavei <0x000c70@gmail.com>
Signed-off-by: Konfekt <Konfekt@users.noreply.github.com>
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
new file mode 100644
index 0000000..9161395
--- /dev/null
+++ b/runtime/autoload/spotbugs.vim
@@ -0,0 +1,250 @@
+" Default pre- and post-compiler actions for SpotBugs
+" Maintainers:  @konfekt and @zzzyxwvut
+" Last Change:  2024 Nov 27
+
+let s:save_cpo = &cpo
+set cpo&vim
+
+if v:version > 900
+
+  function! spotbugs#DeleteClassFiles() abort
+    if !exists('b:spotbugs_class_files')
+      return
+    endif
+
+    for pathname in b:spotbugs_class_files
+      let classname = pathname =~# "^'.\\+\\.class'$"
+          \ ? eval(pathname)
+          \ : pathname
+
+      if classname =~# '\.class$' && filereadable(classname)
+        " Since v9.0.0795.
+        let octad = readblob(classname, 0, 8)
+
+        " Test the magic number and the major version number (45 for v1.0).
+        " Since v9.0.2027.
+        if len(octad) == 8 && octad[0 : 3] == 0zcafe.babe &&
+              \ or((octad[6] << 8), octad[7]) >= 45
+          echomsg printf('Deleting %s: %d', classname, delete(classname))
+        endif
+      endif
+    endfor
+
+    let b:spotbugs_class_files = []
+  endfunction
+
+else
+
+  function! s:DeleteClassFilesWithNewLineCodes(classname) abort
+    " The distribution of "0a"s in class file versions 2560 and 2570:
+    "
+    " 0zca.fe.ba.be.00.00.0a.00    0zca.fe.ba.be.00.00.0a.0a
+    " 0zca.fe.ba.be.00.0a.0a.00    0zca.fe.ba.be.00.0a.0a.0a
+    " 0zca.fe.ba.be.0a.00.0a.00    0zca.fe.ba.be.0a.00.0a.0a
+    " 0zca.fe.ba.be.0a.0a.0a.00    0zca.fe.ba.be.0a.0a.0a.0a
+    let numbers = [0, 0, 0, 0, 0, 0, 0, 0]
+    let offset = 0
+    let lines = readfile(a:classname, 'b', 4)
+
+    " Track NL byte counts to handle files of less than 8 bytes.
+    let nl_cnt = len(lines)
+    " Track non-NL byte counts for "0zca.fe.ba.be.0a.0a.0a.0a".
+    let non_nl_cnt = 0
+
+    for line in lines
+      for idx in range(strlen(line))
+        " Remap NLs to Nuls.
+        let numbers[offset] = (line[idx] == "\n") ? 0 : char2nr(line[idx]) % 256
+        let non_nl_cnt += 1
+        let offset += 1
+
+        if offset > 7
+          break
+        endif
+      endfor
+
+      let nl_cnt -= 1
+
+      if offset > 7 || (nl_cnt < 1 && non_nl_cnt > 4)
+        break
+      endif
+
+      " Reclaim NLs.
+      let numbers[offset] = 10
+      let offset += 1
+
+      if offset > 7
+        break
+      endif
+    endfor
+
+    " Test the magic number and the major version number (45 for v1.0).
+    if offset > 7 && numbers[0] == 0xca && numbers[1] == 0xfe &&
+          \ numbers[2] == 0xba && numbers[3] == 0xbe &&
+          \ (numbers[6] * 256 + numbers[7]) >= 45
+      echomsg printf('Deleting %s: %d', a:classname, delete(a:classname))
+    endif
+  endfunction
+
+  function! spotbugs#DeleteClassFiles() abort
+    if !exists('b:spotbugs_class_files')
+      return
+    endif
+
+    let encoding = &encoding
+
+    try
+      set encoding=latin1
+
+      for pathname in b:spotbugs_class_files
+        let classname = pathname =~# "^'.\\+\\.class'$"
+            \ ? eval(pathname)
+            \ : pathname
+
+        if classname =~# '\.class$' && filereadable(classname)
+          let line = get(readfile(classname, 'b', 1), 0, '')
+          let length = strlen(line)
+
+          " Test the magic number and the major version number (45 for v1.0).
+          if length > 3 && line[0 : 3] == "\xca\xfe\xba\xbe"
+            if length > 7 && ((line[6] == "\n" ? 0 : char2nr(line[6]) % 256) * 256 +
+                    \ (line[7] == "\n" ? 0 : char2nr(line[7]) % 256)) >= 45
+              echomsg printf('Deleting %s: %d', classname, delete(classname))
+            else
+              call s:DeleteClassFilesWithNewLineCodes(classname)
+            endif
+          endif
+        endif
+      endfor
+    finally
+      let &encoding = encoding
+    endtry
+
+    let b:spotbugs_class_files = []
+  endfunction
+
+endif
+
+function! spotbugs#DefaultPostCompilerAction() abort
+  " Since v7.4.191.
+  make %: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
+  endfunction
+
+  function! spotbugs#DefaultPreCompilerTestAction() abort
+    call spotbugs#DeleteClassFiles()
+    compiler maven
+    make test-compile
+  endfunction
+
+  function! spotbugs#DefaultProperties() abort
+    return {
+        \ 'PreCompilerAction':
+            \ function('spotbugs#DefaultPreCompilerAction'),
+        \ 'PreCompilerTestAction':
+            \ function('spotbugs#DefaultPreCompilerTestAction'),
+        \ 'PostCompilerAction':
+            \ function('spotbugs#DefaultPostCompilerAction'),
+        \ 'sourceDirPath':      'src/main/java',
+        \ 'classDirPath':       'target/classes',
+        \ 'testSourceDirPath':  'src/test/java',
+        \ 'testClassDirPath':   'target/test-classes',
+        \ }
+  endfunction
+
+  unlet s:readable s:compiler
+elseif s:readable && s:compiler ==# 'ant' && executable('ant')
+
+  function! spotbugs#DefaultPreCompilerAction() abort
+    call spotbugs#DeleteClassFiles()
+    compiler ant
+    make compile
+  endfunction
+
+  function! spotbugs#DefaultPreCompilerTestAction() abort
+    call spotbugs#DeleteClassFiles()
+    compiler ant
+    make compile-test
+  endfunction
+
+  function! spotbugs#DefaultProperties() abort
+    return {
+        \ 'PreCompilerAction':
+            \ function('spotbugs#DefaultPreCompilerAction'),
+        \ 'PreCompilerTestAction':
+            \ function('spotbugs#DefaultPreCompilerTestAction'),
+        \ 'PostCompilerAction':
+            \ function('spotbugs#DefaultPostCompilerAction'),
+        \ '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')
+
+  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
+    else
+      " Let Javac figure out what files to compile.
+      execute 'make ' . join(map(filter(copy(v:argv),
+          \ "v:val =~# '\\.java\\=$'"),
+          \ 'shellescape(v:val)'), ' ')
+    endif
+  endfunction
+
+  function! spotbugs#DefaultPreCompilerTestAction() abort
+    call spotbugs#DefaultPreCompilerAction()
+  endfunction
+
+  function! spotbugs#DefaultProperties() abort
+    return {
+        \ 'PreCompilerAction':
+            \ function('spotbugs#DefaultPreCompilerAction'),
+        \ 'PreCompilerTestAction':
+            \ function('spotbugs#DefaultPreCompilerTestAction'),
+        \ 'PostCompilerAction':
+            \ function('spotbugs#DefaultPostCompilerAction'),
+        \ }
+  endfunction
+
+  unlet s:readable s:compiler
+else
+
+  function! spotbugs#DefaultPreCompilerAction() abort
+    echomsg printf('Not supported: "%s"', s:compiler)
+  endfunction
+
+  function! spotbugs#DefaultPreCompilerTestAction() abort
+    call spotbugs#DefaultPreCompilerAction()
+  endfunction
+
+  function! spotbugs#DefaultProperties() abort
+    return {}
+  endfunction
+
+  unlet s:readable
+endif
+
+let &cpo = s:save_cpo
+unlet s:save_cpo
+
+" vim: set foldmethod=syntax shiftwidth=2 expandtab:
diff --git a/runtime/compiler/javac.vim b/runtime/compiler/javac.vim
index 9bd4cdf..53cd772 100644
--- a/runtime/compiler/javac.vim
+++ b/runtime/compiler/javac.vim
@@ -1,7 +1,7 @@
 " Vim compiler file
 " Compiler:	Java Development Kit Compiler
 " Maintainer:	Doug Kearns <dougkearns@gmail.com>
-" Last Change:	2024 Jun 14
+" Last Change:	2024 Nov 19 (enable local javac_makeprg_params)
 
 if exists("current_compiler")
   finish
@@ -11,11 +11,7 @@
 let s:cpo_save = &cpo
 set cpo&vim
 
-if exists("g:javac_makeprg_params")
-  execute $'CompilerSet makeprg=javac\ {escape(g:javac_makeprg_params, ' \|"')}'
-else
-  CompilerSet makeprg=javac
-endif
+execute $'CompilerSet makeprg=javac\ {escape(get(b:, 'javac_makeprg_params', get(g:, 'javac_makeprg_params', '')), ' \|"')}'
 
 CompilerSet errorformat=%E%f:%l:\ error:\ %m,
 		       \%W%f:%l:\ warning:\ %m,
diff --git a/runtime/compiler/maven.vim b/runtime/compiler/maven.vim
index ef8d8a6..72e74e3 100644
--- a/runtime/compiler/maven.vim
+++ b/runtime/compiler/maven.vim
@@ -14,7 +14,7 @@
 endif
 let current_compiler = "maven"
 
-CompilerSet makeprg=mvn\ --batch-mode
+execute $'CompilerSet makeprg=mvn\ --batch-mode\ {escape(get(b:, 'maven_makeprg_params', get(g:, 'maven_makeprg_params', '')), ' \|"')}'
 
 " Error message for POM
 CompilerSet errorformat=[FATAL]\ Non-parseable\ POM\ %f:\ %m%\\s%\\+@%.%#line\ %l\\,\ column\ %c%.%#,
diff --git a/runtime/compiler/spotbugs.vim b/runtime/compiler/spotbugs.vim
new file mode 100644
index 0000000..72a5084
--- /dev/null
+++ b/runtime/compiler/spotbugs.vim
@@ -0,0 +1,189 @@
+" Vim compiler file
+" Compiler:     Spotbugs (Java static checker; needs javac compiled classes)
+" Maintainer:   @konfekt and @zzzyxwvut
+" Last Change:  2024 Nov 27
+
+if exists('g:current_compiler') || bufname() !~# '\.java\=$' || wordcount().chars < 9
+  finish
+endif
+
+let s:cpo_save = &cpo
+set cpo&vim
+
+" Unfortunately Spotbugs does not output absolute paths, so you need to
+" pass the directory of the files being checked as `-sourcepath` parameter.
+" The regex, auxpath and glob try to include all dependent classes of the
+" current buffer. See https://github.com/spotbugs/spotbugs/issues/856
+
+" FIXME: When "search()" is used with the "e" flag, it makes no _further_
+" progress after claiming an EOL match (i.e. "\_" or "\n", but not "$").
+" XXX: Omit anonymous class declarations
+let s:keywords = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\|package\)\%(\s\|$\)'
+let s:type_names = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\)\s*\(\K\k*\)\>'
+" Capture ";" for counting a class file directory (see s:package_dir_heads below)
+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')
+
+  function! s:GetDeclaredTypeNames() abort
+    if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
+      return [expand('%:t:r')]
+    endif
+    defer execute('silent! normal! g``')
+    call cursor(1, 1)
+    let type_names = []
+    let lnum = search(s:keywords, 'eW')
+    while lnum > 0
+      let name_attr = synIDattr(synID(lnum, (col('.') - 1), 0), 'name')
+      if name_attr ==# 'javaClassDecl'
+        let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:type_names)
+        if !empty(tokens) | call add(type_names, tokens[1]) | endif
+      elseif name_attr ==# 'javaExternal'
+        let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:package_names)
+        if !empty(tokens) | let s:package = tokens[1] | endif
+      endif
+      let lnum = search(s:keywords, 'eW')
+    endwhile
+    return type_names
+  endfunction
+
+else
+  function! s:GetDeclaredTypeNames() abort
+    if bufname() =~# '\<\%(module\|package\)-info\.java\=$'
+      return [expand('%:t:r')]
+    endif
+    " Undo the unsetting of &hls, see below
+    if &hls
+      defer execute('set hls')
+    endif
+    " Possibly restore the current values for registers '"' and "y", see below
+    defer call('setreg', ['"', getreg('"'), getregtype('"')])
+    defer call('setreg', ['y', getreg('y'), getregtype('y')])
+    defer execute('silent bwipeout')
+    " Copy buffer contents for modification
+    silent %y y
+    new
+    " Apply ":help scratch-buffer" effects and match "$" in Java (generated)
+    " type names (see s:type_names)
+    setlocal iskeyword+=$ buftype=nofile bufhidden=hide noswapfile nohls
+    0put y
+    " Discard text blocks and strings
+    silent keeppatterns %s/\\\@<!"""\_.\{-}\\\@<!"""\|\\"//ge
+    silent keeppatterns %s/".*"//ge
+    " Discard comments
+    silent keeppatterns %s/\/\/.\+$//ge
+    silent keeppatterns %s/\/\*\_.\{-}\*\///ge
+    call cursor(1, 1)
+    let type_names = []
+    let lnum = search(s:keywords, 'eW')
+    while lnum > 0
+      let line = getline(lnum)
+      if line =~# '\<package\>'
+        let tokens = matchlist(line..getline(lnum + 1), s:package_names)
+        if !empty(tokens) | let s:package = tokens[1] | endif
+      else
+        let tokens = matchlist(line..getline(lnum + 1), s:type_names)
+        if !empty(tokens) | call add(type_names, tokens[1]) | endif
+      endif
+      let lnum = search(s:keywords, 'eW')
+    endwhile
+    return type_names
+  endfunction
+endif
+
+if has('win32')
+
+  function! s:GlobClassFiles(src_type_name) abort
+    return glob(a:src_type_name..'$*.class', 1, 1)
+  endfunction
+
+else
+  function! s:GlobClassFiles(src_type_name) abort
+    return glob(a:src_type_name..'\$*.class', 1, 1)
+  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'))
+
+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
+
+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
+endif
+
+function! s:CollectClassFiles() abort
+  " 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 = []
+  " Get all type names in the current buffer and let the filename globbing
+  " discover inner type names from arbitrary type names
+  for type_name in s:GetDeclaredTypeNames()
+    call extend(all_class_files, s:FindClassFiles(src_pathname..type_name))
+  endfor
+  return all_class_files
+endfunction
+
+" 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 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 '..
+    \ join(b:spotbugs_class_files, ' ')
+" Emacs expects doubled line numbers
+setlocal errorformat=%f:%l:%*[0-9]\ %m,%f:-%*[0-9]:-%*[0-9]\ %m
+
+" " This compiler is meant to be used for a single buffer only
+" exe 'CompilerSet makeprg='..escape(&l:makeprg, ' \|"')
+" exe 'CompilerSet errorformat='..escape(&l:errorformat, ' \|"')
+
+delfunction s:CollectClassFiles
+delfunction s:FindClassFiles
+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
+
+" vim: set foldmethod=syntax shiftwidth=2 expandtab:
diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt
index 671630d..5d97f79 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 12
+*quickfix.txt*  For Vim version 9.1.  Last change: 2024 Nov 28
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -1331,10 +1331,117 @@
 JAVAC							*compiler-javac*
 
 Commonly used compiler options can be added to 'makeprg' by setting the
-g:javac_makeprg_params variable.  For example: >
+b/g:javac_makeprg_params variable.  For example: >
 
 	let g:javac_makeprg_params = "-Xlint:all -encoding utf-8"
-<
+
+MAVEN							*compiler-maven*
+
+Commonly used compiler options can be added to 'makeprg' by setting the
+b/g:maven_makeprg_params variable.  For example: >
+
+	let g:maven_makeprg_params = "-DskipTests -U -X"
+
+SPOTBUGS						*compiler-spotbugs*
+
+SpotBugs is a static analysis tool that can be used to find bugs in Java.
+It scans the Java bytecode of all classes in the currently open buffer.
+(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: >
+
+	let b:spotbugs_makeprg_params = "-longBugCodes -effort:max -low"
+
+The global default is "-workHard -experimental".
+
+By default, the class files are searched in the directory where the source
+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): >
+
+	let g:spotbugs_properties = {
+		\ 'sourceDirPath':	'src/main/java',
+		\ 'classDirPath':	'target/classes',
+		\ 'testSourceDirPath':	'src/test/java',
+		\ 'testClassDirPath':	'target/test-classes',
+	\ }
+
+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: >
+
+	let g:spotbugs_properties = {
+		\ 'compiler':		'maven',
+	\ }
+
+This single setting is essentially equivalent to all the settings below, with
+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. >
+
+	let g:spotbugs_properties = {
+		\ 'PreCompilerAction':
+			\ function('spotbugs#DefaultPreCompilerAction'),
+		\ 'PreCompilerTestAction':
+			\ function('spotbugs#DefaultPreCompilerTestAction'),
+		\ 'PostCompilerAction':
+			\ function('spotbugs#DefaultPostCompilerAction'),
+		\ '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
+files for the buffer (and possibly for the whole project) as soon as a Java
+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
+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: >
+
+	function! MavenPreCompilerAction() abort
+		call spotbugs#DeleteClassFiles()
+		compiler maven
+		make compile
+	endfunction
+
+	function! MavenPreCompilerTestAction() abort
+		call spotbugs#DeleteClassFiles()
+		compiler maven
+		make test-compile
+	endfunction
+
+	let g:spotbugs_properties = {
+		\ 'compiler':		'maven',
+		\ 'PreCompilerAction':
+			\ function('MavenPreCompilerAction'),
+		\ 'PreCompilerTestAction':
+			\ function('MavenPreCompilerTestAction'),
+	\ }
+
+Note that all entered custom settings will take precedence over the matching
+default settings in "g:spotbugs_properties".
+
+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].
+
+[0] https://github.com/MarcWeber/vim-addon-local-vimrc/
+
 GNU MAKE						*compiler-make*
 
 Since the default make program is "make", the compiler plugin for make,
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 68465cc..b550550 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -6565,6 +6565,7 @@
 compiler-javac	quickfix.txt	/*compiler-javac*
 compiler-make	quickfix.txt	/*compiler-make*
 compiler-manx	quickfix.txt	/*compiler-manx*
+compiler-maven	quickfix.txt	/*compiler-maven*
 compiler-mypy	quickfix.txt	/*compiler-mypy*
 compiler-pandoc	quickfix.txt	/*compiler-pandoc*
 compiler-perl	quickfix.txt	/*compiler-perl*
@@ -6572,6 +6573,7 @@
 compiler-pyunit	quickfix.txt	/*compiler-pyunit*
 compiler-ruff	quickfix.txt	/*compiler-ruff*
 compiler-select	quickfix.txt	/*compiler-select*
+compiler-spotbugs	quickfix.txt	/*compiler-spotbugs*
 compiler-tex	quickfix.txt	/*compiler-tex*
 compiler-typst	quickfix.txt	/*compiler-typst*
 compiler-vaxada	ft_ada.txt	/*compiler-vaxada*
diff --git a/runtime/ftplugin/java.vim b/runtime/ftplugin/java.vim
index 55b3583..6e12fe2 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 Sep 26
+" Last Change:		2024 Nov 24
 "			2024 Jan 14 by Vim Project (browsefilter)
 "			2024 May 23 by Riley Bruins <ribru17@gmail.com> ('commentstring')
 
@@ -90,10 +90,127 @@
     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
+
+if exists("g:spotbugs_properties") &&
+	    \ filereadable($VIMRUNTIME . '/compiler/spotbugs.vim')
+    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')
+	let s:request += 4
+    endif
+
+    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
+	    let name = expand('%:p')
+
+	    for [path, Action] in a:path_action_pairs
+		if name =~# (path . '.\{-}\.java\=$')
+		    call Action()
+		    break
+		endif
+	    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]]))
+    endif
+
+    if s:request
+	if exists("b:spotbugs_syntax_once")
+	    let s:actions = [{'event': 'BufWritePost'}]
+	else
+	    " XXX: Handle multiple FileType events when vimrc contains more
+	    " than one filetype setting for the language, e.g.:
+	    "	:filetype plugin indent on
+	    "	:autocmd BufRead,BufNewFile *.java setlocal filetype=java ...
+	    " XXX: DO NOT ADD b:spotbugs_syntax_once TO b:undo_ftplugin !
+	    let b:spotbugs_syntax_once = 1
+	    let s:actions = [{
+		    \ 'event': 'Syntax',
+		    \ 'once': 1,
+		    \ }, {
+		    \ 'event': 'BufWritePost',
+		    \ }]
+	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()'
+	    elseif s:request == 4
+		let s:actions[s:idx].cmd = 'compiler spotbugs | ' .
+			\ 'call g:spotbugs_properties.PostCompilerAction()'
+	    elseif s:request == 3 || s:request == 2 || s:request == 1
+		let s:actions[s:idx].cmd = s:dispatcher . 'compiler spotbugs'
+	    else
+		let s:actions[s:idx].cmd = ''
+	    endif
+	endfor
+
+	if !exists("#java_spotbugs")
+	    augroup java_spotbugs
+	    augroup END
+	endif
+
+	" The events are defined in s:actions.
+	silent! autocmd! java_spotbugs BufWritePost <buffer>
+	silent! autocmd! java_spotbugs Syntax <buffer>
+
+	for s:action in s:actions
+	    execute printf('autocmd java_spotbugs %s <buffer> %s',
+		    \ s:action.event,
+		    \ s:action.cmd . (has_key(s:action, 'once')
+			    \ ? printf(' | autocmd! java_spotbugs %s <buffer>',
+				    \ s:action.event)
+			    \ : ''))
+	endfor
+
+	unlet! s:action s:actions s:idx s:dispatcher
+    endif
+
+    unlet s:request
+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>
+    silent! autocmd! java_spotbugs Syntax <buffer>
+endfunction
+
 " Undo the stuff we changed.
-let b:undo_ftplugin = "setlocal suffixes< suffixesadd<" .
-		\     " formatoptions< comments< commentstring< path< includeexpr<" .
-		\     " | unlet! b:browsefilter"
+let b:undo_ftplugin = 'call JavaFileTypeCleanUp() | delfunction JavaFileTypeCleanUp'
 
 " See ":help vim9-mix".
 if !has("vim9script")
@@ -114,6 +231,19 @@
     setlocal suffixesadd<
 endif
 
+if exists("*s:DispatchAction")
+    def! s:DispatchAction(path_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
+	endfor
+    enddef
+endif
+
 " Restore the saved compatibility options.
 let &cpo = s:save_cpo
 unlet s:save_cpo