diff options
-rw-r--r-- | runtime/autoload/spotbugs.vim | 168 | ||||
-rw-r--r-- | runtime/compiler/spotbugs.vim | 155 | ||||
-rw-r--r-- | runtime/doc/quickfix.txt | 154 | ||||
-rw-r--r-- | runtime/ftplugin/java.vim | 270 | ||||
-rw-r--r-- | test/old/testdir/test_compiler.vim | 469 |
5 files changed, 1043 insertions, 173 deletions
diff --git a/runtime/autoload/spotbugs.vim b/runtime/autoload/spotbugs.vim index 9161395794..6fd822d68e 100644 --- a/runtime/autoload/spotbugs.vim +++ b/runtime/autoload/spotbugs.vim @@ -1,11 +1,52 @@ -" 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 -if v:version > 900 +" 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 || has('nvim') function! spotbugs#DeleteClassFiles() abort if !exists('b:spotbugs_class_files') @@ -24,7 +65,9 @@ if v:version > 900 " 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 + " Nvim: no << operator + "\ or((octad[6] << 8), octad[7]) >= 45 + \ or((octad[6] * 256), octad[7]) >= 45 echomsg printf('Deleting %s: %d', classname, delete(classname)) endif endif @@ -127,25 +170,21 @@ endif 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 +195,10 @@ if s:readable && s:compiler ==# 'maven' && executable('mvn') \ 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 +208,13 @@ elseif s:readable && s:compiler ==# 'ant' && executable('ant') 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 +225,55 @@ elseif s:readable && s:compiler ==# 'ant' && executable('ant') \ 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 +285,13 @@ elseif s:readable && s:compiler ==# 'javac' && executable('javac') 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 +306,49 @@ else 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 72a5084976..8ed45f8ee0 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 +" Maintainers: @konfekt and @zzzyxwvut +" Last Change: 2024 Dec 20 if exists('g:current_compiler') || bufname() !~# '\.java\=$' || wordcount().chars < 9 finish @@ -24,8 +24,9 @@ let s:type_names = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\)\s*\(\K\k*\ 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 @@ else 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 +if exists('b:spotbugs_properties') + " Let "ftplugin/java.vim" merge global entries, if any, in buffer-local + " entries + + 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: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 - if !empty(class_files) | break | endif - endfor - return class_files -endfunction + 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), + 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 @@ 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 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 @@ setlocal errorformat=%f:%l:%*[0-9]\ %m,%f:-%*[0-9]:-%*[0-9]\ %m " 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 70082c7835..7aeb494437 100644 --- a/runtime/doc/quickfix.txt +++ b/runtime/doc/quickfix.txt @@ -1335,7 +1335,7 @@ 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: > +"b:" or "g:spotbugs_makeprg_params" variable. For example: >vim let b:spotbugs_makeprg_params = "-longBugCodes -effort:max -low" @@ -1345,22 +1345,25 @@ 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): > +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', @@ -1370,7 +1373,7 @@ 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. > +available. >vim let g:spotbugs_properties = { \ 'PreCompilerAction': @@ -1379,10 +1382,10 @@ available. > \ 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 @@ -1390,23 +1393,106 @@ 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 +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 "PostCompilerAction" is available, "PostCompilerActionExecutor" is also +supported. Its value must be a Funcref pointing to a function that always +declares a single parameter of type string and decides whether |:execute| can +be dispatched on its argument, containing a pending post-compiler action, +after ascertaining the current status of |:cc| (or |:ll|): >vim + + function! GenericPostCompilerActionExecutor(action) abort + try + cc + catch /\<E42:/ + execute a:action + endtry + endfunction + +Complementary, some or all of the available "Pre*Action"s (or "*Pre*Command"s) +may run `:doautocmd java_spotbugs_post User` in their implementations before +|:make| (or its equivalent) to define a once-only |ShellCmdPost| `:autocmd` +that will arrange for "PostCompilerActionExecutor" to be invoked; and then run +`:doautocmd java_spotbugs_post ShellCmdPost` to consume this event: >vim + + function! GenericPreCompilerCommand(arguments) abort + if !exists('g:spotbugs_compilation_done') + doautocmd java_spotbugs_post User + execute 'make ' . a:arguments + " only run doautocmd when :make was synchronous + " see note below + doautocmd java_spotbugs_post ShellCmdPost " XXX: (a) + let g:spotbugs_compilation_done = 1 + else + cc + endif + endfunction + + function! GenericPreCompilerTestCommand(arguments) abort + if !exists('g:spotbugs_test_compilation_done') + doautocmd java_spotbugs_post User + execute 'make ' . a:arguments + " only run doautocmd when :make was synchronous + " see note below + doautocmd java_spotbugs_post ShellCmdPost " XXX: (b) + let g:spotbugs_test_compilation_done = 1 + else + cc + endif + endfunction + + let g:spotbugs_properties = { + \ 'compiler': 'maven', + \ 'DefaultPreCompilerCommand': + \ function('GenericPreCompilerCommand'), + \ 'DefaultPreCompilerTestCommand': + \ function('GenericPreCompilerTestCommand'), + \ 'PostCompilerActionExecutor': + \ function('GenericPostCompilerActionExecutor'), + \ } + +If a command equivalent of `:make` is capable of asynchronous execution and +consuming `ShellCmdPost` events, `:doautocmd java_spotbugs_post ShellCmdPost` +must be removed from such "*Action" (or "*Command") implementations (i.e. the +lines `(a)` and `(b)` in the listed examples) to retain a sequential order for +non-blocking execution, and any notification (see below) must be suppressed. +A `ShellCmdPost` `:autocmd` can be associated with any |:augroup| by assigning +its name to the "augroupForPostCompilerAction" key. + +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 = { @@ -1419,14 +1505,46 @@ project and requests other default Maven settings with the "compiler" entry: > 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 |Signal| event, add these lines to +`~/.config/nvim/after/ftplugin/java.vim`: >vim + + if exists(':SpotBugsDefineBufferAutocmd') == 2 + SpotBugsDefineBufferAutocmd BufWritePost Signal + 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]. + +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/MarcWeber/vim-addon-local-vimrc/ +[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 6e12fe2fe5..cfd25bce6c 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 25 " 2024 Jan 14 by Vim Project (browsefilter) " 2024 May 23 by Riley Bruins <ribru17@gmail.com> ('commentstring') @@ -90,63 +90,155 @@ if (has("gui_win32") || has("gui_gtk")) && !exists("b:browsefilter") 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') + + 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! JavaFileTypeExecuteActionOnce(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 -if exists("g:spotbugs_properties") && - \ filereadable($VIMRUNTIME . '/compiler/spotbugs.vim') + " 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 + if s:SpotBugsHasProperty('PostCompilerAction') + let s:request += 4 endif - if has_key(g:spotbugs_properties, 'PreCompilerTestAction') - let s:dispatcher = 'call g:spotbugs_properties.PreCompilerTestAction() | ' + if s:SpotBugsHasProperty('PreCompilerTestAction') + let s:dispatcher = printf('call call(%s, [])', + \ string(s:SpotBugsGetProperties().PreCompilerTestAction)) let s:request += 2 endif - if has_key(g:spotbugs_properties, 'PostCompilerAction') - let s:request += 4 + 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 @@ if exists("g:spotbugs_properties") && 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,31 +279,76 @@ if exists("g:spotbugs_properties") && 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 JavaFileTypeExecuteActionOnce(%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 + if s:SpotBugsHasProperty('PostCompilerActionExecutor') && + \ (s:request == 7 || s:request == 6 || + \ s:request == 5 || s:request == 4) + let s:augroup = s:SpotBugsGetProperty( + \ 'augroupForPostCompilerAction', + \ 'java_spotbugs_post') + let s:augroup = !empty(s:augroup) ? s:augroup : 'java_spotbugs_post' + + for s:candidate in ['java_spotbugs_post', s:augroup] + if !exists("#" . s:candidate) + execute printf('augroup %s | augroup END', s:candidate) + endif + endfor + + silent! autocmd! java_spotbugs_post User <buffer> + + " Define a User ":autocmd" to define a once-only ShellCmdPost + " ":autocmd" that will invoke "PostCompilerActionExecutor" and let + " it decide whether to proceed with ":compiler spotbugs" etc.; and + " seek explicit synchronisation with ":doautocmd ShellCmdPost" by + " omitting "nested" for "java_spotbugs_post" and "java_spotbugs". + execute printf('autocmd java_spotbugs_post User <buffer> ' . + \ 'call JavaFileTypeExecuteActionOnce(%s, %s)', + \ string(printf('autocmd! %s ShellCmdPost <buffer>', s:augroup)), + \ string(printf('autocmd %s ShellCmdPost <buffer> ' . + \ 'call JavaFileTypeExecuteActionOnce(%s, %s)', + \ s:augroup, + \ string(printf('autocmd! %s ShellCmdPost <buffer>', s:augroup)), + \ string(printf('call call(%s, [%s])', + \ string(s:SpotBugsGetProperties().PostCompilerActionExecutor), + \ string(printf('compiler spotbugs | call call(%s, [])', + \ string(s:SpotBugsGetProperties().PostCompilerAction)))))))) + endif + + unlet! s:candidate s:augroup 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 ":autocmd" removals may be misparsed as an ":autocmd". + " A _once-only_ ShellCmdPost ":autocmd" is always a call-site definition. + silent! autocmd! java_spotbugs User <buffer> silent! autocmd! java_spotbugs Syntax <buffer> + silent! autocmd! java_spotbugs_post User <buffer> endfunction " Undo the stuff we changed. @@ -232,14 +374,16 @@ if exists("s:zip_func_upgradable") 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 diff --git a/test/old/testdir/test_compiler.vim b/test/old/testdir/test_compiler.vim index 3ad6b365de..1310cbadfc 100644 --- a/test/old/testdir/test_compiler.vim +++ b/test/old/testdir/test_compiler.vim @@ -18,6 +18,8 @@ func Test_compiler() endif e Xfoo.pl + " Play nice with other tests. + defer setqflist([]) compiler perl call assert_equal('perl', b:current_compiler) call assert_fails('let g:current_compiler', 'E121:') @@ -91,6 +93,7 @@ func s:SpotBugsParseFilterMakePrg(dirname, makeprg) endif let offset += 1 + strlen('-sourcepath') let result.sourcepath = matchstr(strpart(a:makeprg, offset), '.\{-}\ze[ \t]') + let offset += 1 + strlen(result.sourcepath) " Get the class file arguments, dropping the pathname prefix. let offset = stridx(a:makeprg, a:dirname, offset) @@ -140,7 +143,8 @@ func Test_compiler_spotbugs_makeprg() " THE EXPECTED RESULTS. let results = {} let results['Xspotbugs/src/tests/𐌂1.java'] = { - \ 'sourcepath': '%:p:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/𐌂1.java', + \ ':p:h:S')}, \ 'classfiles': sort([ \ 'Xspotbugs/tests/𐌂1$1.class', \ 'Xspotbugs/tests/𐌂1$1𐌉𐌉1.class', @@ -153,11 +157,12 @@ func Test_compiler_spotbugs_makeprg() \ } " No class file for an empty source file even with "-Xpkginfo:always". let results['Xspotbugs/src/tests/package-info.java'] = { - \ 'sourcepath': '', + \ 'Sourcepath': {-> ''}, \ 'classfiles': [], \ } let results['Xspotbugs/src/tests/α/𐌂1.java'] = { - \ 'sourcepath': '%:p:h:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/𐌂1.java', + \ ':p:h:h:S')}, \ 'classfiles': sort([ \ 'Xspotbugs/tests/α/𐌂1$1.class', \ 'Xspotbugs/tests/α/𐌂1$1𐌉𐌉1.class', @@ -169,11 +174,13 @@ func Test_compiler_spotbugs_makeprg() \ 'Xspotbugs/tests/α/𐌂2.class']), \ } let results['Xspotbugs/src/tests/α/package-info.java'] = { - \ 'sourcepath': '%:p:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/package-info.java', + \ ':p:h:S')}, \ 'classfiles': ['Xspotbugs/tests/α/package-info.class'], \ } let results['Xspotbugs/src/tests/α/β/𐌂1.java'] = { - \ 'sourcepath': '%:p:h:h:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/β/𐌂1.java', + \ ':p:h:h:h:S')}, \ 'classfiles': sort([ \ 'Xspotbugs/tests/α/β/𐌂1$1.class', \ 'Xspotbugs/tests/α/β/𐌂1$1𐌉𐌉1.class', @@ -185,11 +192,13 @@ func Test_compiler_spotbugs_makeprg() \ 'Xspotbugs/tests/α/β/𐌂2.class']), \ } let results['Xspotbugs/src/tests/α/β/package-info.java'] = { - \ 'sourcepath': '%:p:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/β/package-info.java', + \ ':p:h:S')}, \ 'classfiles': ['Xspotbugs/tests/α/β/package-info.class'], \ } let results['Xspotbugs/src/tests/α/β/γ/𐌂1.java'] = { - \ 'sourcepath': '%:p:h:h:h:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/β/γ/𐌂1.java', + \ ':p:h:h:h:h:S')}, \ 'classfiles': sort([ \ 'Xspotbugs/tests/α/β/γ/𐌂1$1.class', \ 'Xspotbugs/tests/α/β/γ/𐌂1$1𐌉𐌉1.class', @@ -201,11 +210,13 @@ func Test_compiler_spotbugs_makeprg() \ 'Xspotbugs/tests/α/β/γ/𐌂2.class']), \ } let results['Xspotbugs/src/tests/α/β/γ/package-info.java'] = { - \ 'sourcepath': '%:p:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/β/γ/package-info.java', + \ ':p:h:S')}, \ 'classfiles': ['Xspotbugs/tests/α/β/γ/package-info.class'], \ } let results['Xspotbugs/src/tests/α/β/γ/δ/𐌂1.java'] = { - \ 'sourcepath': '%:p:h:h:h:h:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/β/γ/δ/𐌂1.java', + \ ':p:h:h:h:h:h:S')}, \ 'classfiles': sort([ \ 'Xspotbugs/tests/α/β/γ/δ/𐌂1$1.class', \ 'Xspotbugs/tests/α/β/γ/δ/𐌂1$1𐌉𐌉1.class', @@ -217,14 +228,15 @@ func Test_compiler_spotbugs_makeprg() \ 'Xspotbugs/tests/α/β/γ/δ/𐌂2.class']), \ } let results['Xspotbugs/src/tests/α/β/γ/δ/package-info.java'] = { - \ 'sourcepath': '%:p:h:S', + \ 'Sourcepath': {-> fnamemodify('Xspotbugs/src/tests/α/β/γ/δ/package-info.java', + \ ':p:h:S')}, \ 'classfiles': ['Xspotbugs/tests/α/β/γ/δ/package-info.class'], \ } " MAKE CLASS FILES DISCOVERABLE! let g:spotbugs_properties = { - \ 'sourceDirPath': 'src/tests', - \ 'classDirPath': 'tests', + \ 'sourceDirPath': ['src/tests'], + \ 'classDirPath': ['tests'], \ } call assert_true(has_key(s:SpotBugsParseFilterMakePrg('Xspotbugs', ''), 'sourcepath')) @@ -249,20 +261,22 @@ func Test_compiler_spotbugs_makeprg() let package_file = src_dir .. 'package-info.java' call writefile([package], src_dir .. 'package-info.java') - for s in ['on', 'off'] + " Note that using "off" for the first _outer_ iteration is preferable + " because only then "hlexists()" may be 0 (see "compiler/spotbugs.vim"). + for s in ['off', 'on'] execute 'syntax ' .. s execute 'edit ' .. type_file compiler spotbugs let result = s:SpotBugsParseFilterMakePrg('Xspotbugs', &l:makeprg) - call assert_equal(results[type_file].sourcepath, result.sourcepath) + call assert_equal(results[type_file].Sourcepath(), result.sourcepath) call assert_equal(results[type_file].classfiles, result.classfiles) bwipeout execute 'edit ' .. package_file compiler spotbugs let result = s:SpotBugsParseFilterMakePrg('Xspotbugs', &l:makeprg) - call assert_equal(results[package_file].sourcepath, result.sourcepath) + call assert_equal(results[package_file].Sourcepath(), result.sourcepath) call assert_equal(results[package_file].classfiles, result.classfiles) bwipeout endfor @@ -271,4 +285,429 @@ func Test_compiler_spotbugs_makeprg() let &shellslash = save_shellslash endfunc +func s:SpotBugsBeforeFileTypeTryPluginAndClearCache(state) + " Ponder over "extend(spotbugs#DefaultProperties(), g:spotbugs_properties)" + " in "ftplugin/java.vim". + let g:spotbugs#state = a:state + runtime autoload/spotbugs.vim +endfunc + +func Test_compiler_spotbugs_properties() + let save_shellslash = &shellslash + set shellslash + setlocal makeprg= + filetype plugin on + + call assert_true(mkdir('Xspotbugs/src', 'pR')) + call assert_true(mkdir('Xspotbugs/tests', 'pR')) + let type_file = 'Xspotbugs/src/𐌄.java' + let test_file = 'Xspotbugs/tests/𐌄$.java' + call writefile(['enum 𐌄{}'], type_file) + call writefile(['class 𐌄${}'], test_file) + + " TEST INTEGRATION WITH A BOGUS COMPILER PLUGIN. + if !filereadable($VIMRUNTIME .. '/compiler/foo.vim') && !executable('foo') + let g:spotbugs_properties = {'compiler': 'foo'} + " XXX: In case this "if" block is no longer first. + call s:SpotBugsBeforeFileTypeTryPluginAndClearCache({ + \ 'compiler': g:spotbugs_properties.compiler, + \ }) + execute 'edit ' .. type_file + call assert_equal('java', &l:filetype) + " This variable will indefinitely keep the compiler name. + call assert_equal('foo', g:spotbugs#state.compiler) + " The "compiler" entry should be gone after FileType and default entries + " should only appear for a supported compiler. + call assert_false(has_key(g:spotbugs_properties, 'compiler')) + call assert_true(empty(g:spotbugs_properties)) + " Query default implementations. + call assert_true(exists('*spotbugs#DefaultProperties')) + call assert_true(exists('*spotbugs#DefaultPreCompilerAction')) + call assert_true(exists('*spotbugs#DefaultPreCompilerTestAction')) + call assert_true(empty(spotbugs#DefaultProperties())) + " Get a ":message". + redir => out + call spotbugs#DefaultPreCompilerAction() + redir END + call assert_equal('Not supported: "foo"', out[stridx(out, 'Not') :]) + " Get a ":message". + redir => out + call spotbugs#DefaultPreCompilerTestAction() + redir END + call assert_equal('Not supported: "foo"', out[stridx(out, 'Not') :]) + " No ":autocmd"s without one of "PreCompiler*Action", "PostCompilerAction". + call assert_false(exists('#java_spotbugs')) + bwipeout + endif + + let s:spotbugs_results = { + \ 'preActionDone': 0, + \ 'preTestActionDone': 0, + \ 'preTestLocalActionDone': 0, + \ 'postActionDone': 0, + \ 'preCommandArguments': '', + \ 'preTestCommandArguments': '', + \ 'postCommandArguments': '', + \ } + defer execute('unlet s:spotbugs_results') + + func! g:SpotBugsPreAction() abort + let s:spotbugs_results.preActionDone = 1 + " XXX: Notify the spotbugs compiler about success or failure. + cc + endfunc + defer execute('delfunction g:SpotBugsPreAction') + + func! g:SpotBugsPreTestAction() abort + let s:spotbugs_results.preTestActionDone = 1 + " XXX: Let see compilation fail. + throw 'Oops' + endfunc + defer execute('delfunction g:SpotBugsPreTestAction') + + func! g:SpotBugsPreTestLocalAction() abort + let s:spotbugs_results.preTestLocalActionDone = 1 + " XXX: Notify the spotbugs compiler about success or failure. + cc + endfunc + defer execute('delfunction g:SpotBugsPreTestLocalAction') + + func! g:SpotBugsPostAction() abort + let s:spotbugs_results.postActionDone = 1 + endfunc + defer execute('delfunction g:SpotBugsPostAction') + + func! g:SpotBugsPreCommand(arguments) abort + let s:spotbugs_results.preActionDone = 1 + let s:spotbugs_results.preCommandArguments = a:arguments + " XXX: Notify the spotbugs compiler about success or failure. + cc + endfunc + defer execute('delfunction g:SpotBugsPreCommand') + + func! g:SpotBugsPreTestCommand(arguments) abort + let s:spotbugs_results.preTestActionDone = 1 + let s:spotbugs_results.preTestCommandArguments = a:arguments + " XXX: Notify the spotbugs compiler about success or failure. + cc + endfunc + defer execute('delfunction g:SpotBugsPreTestCommand') + + func! g:SpotBugsPostCommand(arguments) abort + let s:spotbugs_results.postActionDone = 1 + let s:spotbugs_results.postCommandArguments = a:arguments + endfunc + defer execute('delfunction g:SpotBugsPostCommand') + + func! g:SpotBugsPostCompilerActionExecutor(action) abort + try + " XXX: Notify the spotbugs compiler about success or failure. + cc + catch /\<E42:/ + execute a:action + endtry + endfunc + defer execute('delfunction g:SpotBugsPostCompilerActionExecutor') + + " TEST INTEGRATION WITH A SUPPORTED COMPILER PLUGIN. + if filereadable($VIMRUNTIME .. '/compiler/maven.vim') + let save_PATH = $PATH + if !executable('mvn') + if has('win32') + let $PATH = 'Xspotbugs;' .. $PATH + " This is what ":help executable()" suggests. + call writefile([], 'Xspotbugs/mvn.cmd') + else + let $PATH = 'Xspotbugs:' .. $PATH + call writefile([], 'Xspotbugs/mvn') + call setfperm('Xspotbugs/mvn', 'rwx------') + endif + endif + + let g:spotbugs_properties = { + \ 'compiler': 'maven', + \ 'PreCompilerAction': function('g:SpotBugsPreAction'), + \ 'PreCompilerTestAction': function('g:SpotBugsPreTestAction'), + \ 'PostCompilerAction': function('g:SpotBugsPostAction'), + \ } + " XXX: In case this is a runner-up ":edit". + call s:SpotBugsBeforeFileTypeTryPluginAndClearCache({ + \ 'compiler': g:spotbugs_properties.compiler, + \ }) + execute 'edit ' .. type_file + call assert_equal('java', &l:filetype) + call assert_equal('maven', g:spotbugs#state.compiler) + call assert_false(has_key(g:spotbugs_properties, 'compiler')) + call assert_false(empty(g:spotbugs_properties)) + " Query default implementations. + call assert_true(exists('*spotbugs#DefaultProperties')) + call assert_equal(sort([ + \ 'PreCompilerAction', + \ 'PreCompilerTestAction', + \ 'PostCompilerAction', + \ 'sourceDirPath', + \ 'classDirPath', + \ 'testSourceDirPath', + \ 'testClassDirPath', + \ ]), + \ sort(keys(spotbugs#DefaultProperties()))) + " Some ":autocmd"s with one of "PreCompiler*Action", "PostCompilerAction". + call assert_true(exists('#java_spotbugs')) + call assert_true(exists('#java_spotbugs#Syntax')) + call assert_true(exists('#java_spotbugs#User')) + call assert_equal(2, exists(':SpotBugsDefineBufferAutocmd')) + " SpotBugsDefineBufferAutocmd SigUSR1 User SigUSR1 User SigUSR1 User + " call assert_true(exists('#java_spotbugs#SigUSR1')) + SpotBugsDefineBufferAutocmd Signal User Signal User Signal User + call assert_true(exists('#java_spotbugs#Signal')) + call assert_true(exists('#java_spotbugs#Syntax')) + call assert_true(exists('#java_spotbugs#User')) + call assert_equal(2, exists(':SpotBugsRemoveBufferAutocmd')) + " SpotBugsRemoveBufferAutocmd SigUSR1 User SigUSR1 User UserGettingBored + " call assert_false(exists('#java_spotbugs#SigUSR1')) + SpotBugsRemoveBufferAutocmd Signal User Signal User UserGettingBored + call assert_false(exists('#java_spotbugs#Signal')) + call assert_true(exists('#java_spotbugs#Syntax')) + call assert_true(exists('#java_spotbugs#User')) + + let s:spotbugs_results.preActionDone = 0 + let s:spotbugs_results.preTestActionDone = 0 + let s:spotbugs_results.postActionDone = 0 + + doautocmd java_spotbugs Syntax + call assert_false(exists('#java_spotbugs#Syntax')) + + " No match: "type_file !~# 'src/main/java'". + call assert_false(s:spotbugs_results.preActionDone) + " No match: "type_file !~# 'src/test/java'". + call assert_false(s:spotbugs_results.preTestActionDone) + " No pre-match, no post-action. + call assert_false(s:spotbugs_results.postActionDone) + " Without a match, confirm that ":compiler spotbugs" has NOT run. + call assert_true(empty(&l:makeprg)) + + let s:spotbugs_results.preActionDone = 0 + let s:spotbugs_results.preTestActionDone = 0 + let s:spotbugs_results.postActionDone = 0 + " Update path entries. (Note that we cannot use just "src" because there + " is another "src" directory nearer the filesystem root directory, i.e. + " "vim/vim/src/testdir/Xspotbugs/src", and "s:DispatchAction()" (see + " "ftplugin/java.vim") will match "vim/vim/src/testdir/Xspotbugs/tests" + " against "src".) + let g:spotbugs_properties.sourceDirPath = ['Xspotbugs/src'] + let g:spotbugs_properties.classDirPath = ['Xspotbugs/src'] + let g:spotbugs_properties.testSourceDirPath = ['tests'] + let g:spotbugs_properties.testClassDirPath = ['tests'] + + doautocmd java_spotbugs User + " No match: "type_file !~# 'src/main/java'" (with old "*DirPath" values + " cached). + call assert_false(s:spotbugs_results.preActionDone) + " No match: "type_file !~# 'src/test/java'" (with old "*DirPath" values + " cached). + call assert_false(s:spotbugs_results.preTestActionDone) + " No pre-match, no post-action. + call assert_false(s:spotbugs_results.postActionDone) + " Without a match, confirm that ":compiler spotbugs" has NOT run. + call assert_true(empty(&l:makeprg)) + + let s:spotbugs_results.preActionDone = 0 + let s:spotbugs_results.preTestActionDone = 0 + let s:spotbugs_results.postActionDone = 0 + " XXX: Re-build ":autocmd"s from scratch with new values applied. + doautocmd FileType + + call assert_true(exists('b:spotbugs_syntax_once')) + doautocmd java_spotbugs User + " A match: "type_file =~# 'Xspotbugs/src'" (with new "*DirPath" values + " cached). + call assert_true(s:spotbugs_results.preActionDone) + " No match: "type_file !~# 'tests'" (with new "*DirPath" values cached). + call assert_false(s:spotbugs_results.preTestActionDone) + " For a pre-match, a post-action. + call assert_true(s:spotbugs_results.postActionDone) + + " With a match, confirm that ":compiler spotbugs" has run. + if has('win32') + call assert_match('^spotbugs\.bat\s', &l:makeprg) + else + call assert_match('^spotbugs\s', &l:makeprg) + endif + + bwipeout + setlocal makeprg= + let s:spotbugs_results.preActionDone = 0 + let s:spotbugs_results.preTestActionDone = 0 + let s:spotbugs_results.preTestLocalActionDone = 0 + let s:spotbugs_results.postActionDone = 0 + + execute 'edit ' .. test_file + " Prepare a buffer-local, incomplete variant of properties, relying on + " "ftplugin/java.vim" to take care of merging in unique entries, if any, + " from "g:spotbugs_properties". + let b:spotbugs_properties = { + \ 'PreCompilerTestAction': function('g:SpotBugsPreTestLocalAction'), + \ } + call assert_equal('java', &l:filetype) + call assert_true(exists('#java_spotbugs')) + call assert_true(exists('#java_spotbugs#Syntax')) + call assert_true(exists('#java_spotbugs#User')) + call assert_fails('doautocmd java_spotbugs Syntax', 'Oops') + call assert_false(exists('#java_spotbugs#Syntax')) + " No match: "test_file !~# 'Xspotbugs/src'". + call assert_false(s:spotbugs_results.preActionDone) + " A match: "test_file =~# 'tests'". + call assert_true(s:spotbugs_results.preTestActionDone) + call assert_false(s:spotbugs_results.preTestLocalActionDone) + " No action after pre-failure (the thrown "Oops" doesn't qualify for ":cc"). + call assert_false(s:spotbugs_results.postActionDone) + " No ":compiler spotbugs" will be run after pre-failure. + call assert_true(empty(&l:makeprg)) + + let s:spotbugs_results.preActionDone = 0 + let s:spotbugs_results.preTestActionDone = 0 + let s:spotbugs_results.preTestLocalActionDone = 0 + let s:spotbugs_results.postActionDone = 0 + " XXX: Re-build ":autocmd"s from scratch with buffer-local values applied. + doautocmd FileType + + call assert_true(exists('b:spotbugs_syntax_once')) + doautocmd java_spotbugs User + " No match: "test_file !~# 'Xspotbugs/src'". + call assert_false(s:spotbugs_results.preActionDone) + " A match: "test_file =~# 'tests'". + call assert_true(s:spotbugs_results.preTestLocalActionDone) + call assert_false(s:spotbugs_results.preTestActionDone) + " For a pre-match, a post-action. + call assert_true(s:spotbugs_results.postActionDone) + + " With a match, confirm that ":compiler spotbugs" has run. + if has('win32') + call assert_match('^spotbugs\.bat\s', &l:makeprg) + else + call assert_match('^spotbugs\s', &l:makeprg) + endif + + setlocal makeprg= + let s:spotbugs_results.preActionDone = 0 + let s:spotbugs_results.preTestActionDone = 0 + let s:spotbugs_results.preTestLocalActionDone = 0 + let s:spotbugs_results.postActionDone = 0 + let s:spotbugs_results.preCommandArguments = '' + let s:spotbugs_results.preTestCommandArguments = '' + let s:spotbugs_results.postCommandArguments = '' + " XXX: Compose the assigned "*Command"s with the default Maven "*Action"s. + let b:spotbugs_properties = { + \ 'compiler': 'maven', + \ 'DefaultPreCompilerTestCommand': function('g:SpotBugsPreTestCommand'), + \ 'DefaultPreCompilerCommand': function('g:SpotBugsPreCommand'), + \ 'DefaultPostCompilerCommand': function('g:SpotBugsPostCommand'), + \ 'PostCompilerActionExecutor': function('g:SpotBugsPostCompilerActionExecutor'), + \ 'augroupForPostCompilerAction': 'java_spotbugs_test', + \ 'sourceDirPath': ['Xspotbugs/src'], + \ 'classDirPath': ['Xspotbugs/src'], + \ 'testSourceDirPath': ['tests'], + \ 'testClassDirPath': ['tests'], + \ } + unlet g:spotbugs_properties + " XXX: Re-build ":autocmd"s from scratch with buffer-local values applied. + call s:SpotBugsBeforeFileTypeTryPluginAndClearCache({ + \ 'compiler': b:spotbugs_properties.compiler, + \ 'commands': { + \ 'DefaultPreCompilerTestCommand': + \ b:spotbugs_properties.DefaultPreCompilerTestCommand, + \ 'DefaultPreCompilerCommand': + \ b:spotbugs_properties.DefaultPreCompilerCommand, + \ 'DefaultPostCompilerCommand': + \ b:spotbugs_properties.DefaultPostCompilerCommand, + \ }, + \ }) + doautocmd FileType + + call assert_equal('maven', g:spotbugs#state.compiler) + call assert_equal(sort([ + \ 'DefaultPreCompilerTestCommand', + \ 'DefaultPreCompilerCommand', + \ 'DefaultPostCompilerCommand', + \ ]), + \ sort(keys(g:spotbugs#state.commands))) + call assert_true(exists('b:spotbugs_syntax_once')) + doautocmd java_spotbugs User + " No match: "test_file !~# 'Xspotbugs/src'". + call assert_false(s:spotbugs_results.preActionDone) + call assert_true(empty(s:spotbugs_results.preCommandArguments)) + " A match: "test_file =~# 'tests'". + call assert_true(s:spotbugs_results.preTestActionDone) + call assert_equal('test-compile', s:spotbugs_results.preTestCommandArguments) + " For a pre-match, a post-action. + call assert_true(s:spotbugs_results.postActionDone) + call assert_equal('%:S', s:spotbugs_results.postCommandArguments) + + " With a match, confirm that ":compiler spotbugs" has run. + if has('win32') + call assert_match('^spotbugs\.bat\s', &l:makeprg) + else + call assert_match('^spotbugs\s', &l:makeprg) + endif + + setlocal makeprg= + let s:spotbugs_results.preActionDone = 0 + let s:spotbugs_results.preTestActionOtherDone = 0 + let s:spotbugs_results.preTestLocalActionDone = 0 + let s:spotbugs_results.postActionDone = 0 + let s:spotbugs_results.preCommandArguments = '' + let s:spotbugs_results.preTestCommandArguments = '' + let s:spotbugs_results.postCommandArguments = '' + + " When "PostCompilerActionExecutor", "Pre*Action" and/or "Pre*TestAction", + " and "Post*Action" are available, "#java_spotbugs_post" must be defined. + call assert_true(exists('#java_spotbugs_post')) + call assert_true(exists('#java_spotbugs_post#User')) + call assert_false(exists('#java_spotbugs_post#ShellCmdPost')) + call assert_false(exists('#java_spotbugs_test#ShellCmdPost')) + + " Re-link a Funcref on the fly. + func! g:SpotBugsPreTestCommand(arguments) abort + let s:spotbugs_results.preTestActionOtherDone = 1 + let s:spotbugs_results.preTestCommandArguments = a:arguments + " Define a once-only ":autocmd" for "#java_spotbugs_test#ShellCmdPost". + doautocmd java_spotbugs_post User + " XXX: Do NOT use ":cc" to notify the spotbugs compiler about success or + " failure, and assume the transfer of control to a ShellCmdPost command. + endfunc + + doautocmd java_spotbugs User + " No match: "test_file !~# 'Xspotbugs/src'". + call assert_false(s:spotbugs_results.preActionDone) + call assert_true(empty(s:spotbugs_results.preCommandArguments)) + " A match: "test_file =~# 'tests'". + call assert_true(s:spotbugs_results.preTestActionOtherDone) + call assert_equal('test-compile', s:spotbugs_results.preTestCommandArguments) + " For a pre-match, no post-action (without ":cc") UNLESS a ShellCmdPost + " event is consumed whose command will invoke "PostCompilerActionExecutor" + " and the latter will accept a post-compiler action argument. + call assert_false(s:spotbugs_results.postActionDone) + call assert_true(exists('#java_spotbugs_test#ShellCmdPost')) + doautocmd ShellCmdPost + call assert_false(exists('#java_spotbugs_test#ShellCmdPost')) + call assert_true(s:spotbugs_results.postActionDone) + call assert_equal('%:S', s:spotbugs_results.postCommandArguments) + + " With a match, confirm that ":compiler spotbugs" has run. + if has('win32') + call assert_match('^spotbugs\.bat\s', &l:makeprg) + else + call assert_match('^spotbugs\s', &l:makeprg) + endif + + bwipeout + setlocal makeprg= + let $PATH = save_PATH + endif + + filetype plugin off + setlocal makeprg= + let &shellslash = save_shellslash +endfunc + " vim: shiftwidth=2 sts=2 expandtab |