読者です 読者をやめる 読者になる 読者になる

辞書関数対応版:Vim scriptをsourceしてエラーがあればquickfixに表示する

vim

前回:関数対応版:Vim scriptをsourceしてエラーがあればquickfixに表示する - ぼっち勉強会

辞書関数のエラー行数も取れないかチャレンジしてみた。
実用性がどうとかよりも、もはや自分のVim script力への挑戦。
いつも通り動作環境は自分のMacVim+簡単なサンプルだけ。
ソースが長くなってきたので先に実行結果と説明から。

実行結果

説明

ここまで説明がなかったので簡単に説明を。
まず、自分の環境で以下のコードをsourceしてみた場合のエラーから。

one

function! s:hoge()
  two
endfunction
call s:hoge()

let s:dict = {}
function! s:dict.aaa()
  hey!
endfunction
call s:dict.aaa()

/Users/kanno/tmp/tmp4.vim の処理中にエラーが検出されました:
行 1:
E492: エディタのコマンドではありません: one
function 168_hoge の処理中にエラーが検出されました:
行 1:
E492: エディタのコマンドではありません: two
function 626 の処理中にエラーが検出されました:
行 1:
E492: エディタのコマンドではありません: hey!

このことからエラーフォーマットが次のようになっていることが分かる。

{ファイル名or関数名} の処理中にエラーが検出されました:
行 {行番号}:
{エラーメッセージ}

関数外のエラー

そのため、最初の対応では各行を繰り返して力技で算出していた。

  • 対象ファイルをsourceして出力結果を記録
  • 出力結果を一行ずつグルグル回す
  • 「行」で始まった場合はそこに書かれている行番号を記録(A)
  • 「E」で始まった場合は(A)の行番号、ファイル名やメッセージと共に記録

最後にsetqflistでquickfixに登録。
関数外で発生しているエラーならこれでもよかった。
ちなみにこの程度ならerrorformatの設定だけで動くかもしれない。

関数内のエラー

問題は、表示される{行番号}が実はスコープ毎の相対値となっていること。
関数内のエラーの場合は関数定義の始まりから見た相対的な行番号となっている。
なので、次の対応ではさらに力技で算出した。

  • 対象のファイルを一度readfileで読み込んでおく(A)
  • 対象ファイルをsourceして出力結果を記録
  • 出力結果を一行ずつグルグル回す
  • 「function」で始まった場合は関数名を抽出して、(A)と照らし合わせて関数が定義されている行番号を取り出して記録(B)
  • 「行」で始まった場合はそこに書かれている行番号を記録(C)
  • 「E」で始まった場合は(B)と(C)を足して絶対的な行番号を割り出し、ファイル名やメッセージと共に記録

これで関数内のエラーも検知できるようになった。

辞書関数内のエラー

続いての問題で、今回対応したのが辞書関数。
辞書関数の何がやっかいかというと、Vim内部の関数名としては数字の羅列でしかないので、
上述したソース文字列とのマッチングでは検出できないこと。
「function 626 の処理中にエラーが検出されました:」の626が関数名となっている。

辞書関数の定義を参照すること自体は、「function {626}」と打ったり、「function s:dict.aaa」とすれば表示される。
「function {626}」については:help anonymous-functionにて

番号付き関数でエラーが発生したときは、あるトリックを使うことで発生源を確認できます

とドヤ風に書かれている。

":function {626} の出力結果
   function 626()
1
2    hey!
   endfunction

とまあ定義は出るんだけど、この程度ではどのファイルの何行目で定義されてるか辿れない。

で、仕方ないので上述した(A)のタイミングで関数は全部「function 関数名」して実際の関数名をマッピングしようとした。
でもそれってvimrcのバッファ(?)側で「function s:dict.aaa」とかを実行することになってしまい、
スコープ的に「s:dictなんて知りません」と怒られてしまった。
もうやだーってなったけど、結果的にはいつも通り力技で解決した。

「実際の関数名をマッピングするためのフックを対象ファイルに加えた上でsourceする」

  • 関数名のマッピングを作成するフック用の文字列を生成する(A)
  • 対象のファイルを一度readfileで読み込み、末尾に(A)を追加する。この文字列にてtempfileに書き込む(B)
  • (B)の内容をsourceして出力結果を記録
    • このときフックも実行され、g:__func_lnums__という変数が{関数名:行番号}で生成される(C)
  • 出力結果を一行ずつグルグル回す
  • 「function」で始まった場合は関数名を抽出して、(C)と照らし合わせて行番号を取り出して記録(D)
  • 「行」で始まった場合はそこに書かれている行番号を記録(E)
  • 「E」で始まった場合は(D)と(E)を足して絶対的な行番号を割り出し、ファイル名やメッセージと共に記録

これで動いているような感じだけど、もっと綺麗に書けないものかと思う。

ソースコード

vimrcに以下を追加。
なお、エラーメッセージは日本語と仮定する。英語は考慮してない。
先日アップされたこちらの記事をもとにいくつか修正もした。
Vim script のエラーを出力する quickrun-outputter - C++でゲームプログラミング

" 指定したVim scriptをsourceしてエラーをquickfixに表示
function! s:vimqf(file)
  unlet! g:__func_lnums__
  try
    call setqflist(s:vimqf_qflist(a:file), 'r')
    cwindow
    silent! doautocmd QuickFixCmdPost make
  finally
    unlet! g:__func_lnums__
  endtry
endfunction

function! s:vimqf_qflist(file)
  let srclines = readfile(a:file, 'b')
  let qflist = []
  let start_pos    = 0
  let relative_num = 0
  for l in s:vimqf_source(s:vimqf_hook_file(srclines))
    "ex)function <SNR>175_hoge の処理中にエラーが検出されました:
    if l =~ '^function'
      let start_pos = s:vimqf_func_define_linenum(srclines, l)
    "ex)行    1:
    elseif l =~ '^行'
      let relative_num = matchstr(l, '行\s*\zs\d*\ze')
    "ex)E492: エディタのコマンドではありません:   et s:str = ''
    elseif l =~ '^E'
      call add(qflist, {
            \ 'filename': a:file,
            \ 'lnum'    : start_pos + relative_num,
            \ 'text'    : l
            \ })
      let start_pos    = 0
      let relative_num = 0
    endif
  endfor
  return qflist
endfunction

function! s:vimqf_hook_file(srclines)
  let tempfile = tempname()
  call writefile(extend(a:srclines, s:vimqf_hook_lines()), tempfile, 'b')
  return tempfile
endfunction

function! s:vimqf_source(file)
  let tempfile = tempname()
  let save_vfile = &verbosefile
  let &verbosefile = tempfile

  try
    silent! execute ':source ' . a:file
  finally
    if &verbosefile ==# tempfile
      let &verbosefile = save_vfile
    endif
  endtry

  let messages = ''
  if filereadable(tempfile)
    let messages .= join(readfile(tempfile, 'b'), "\n")
    call delete(tempfile)
  endif
  return split(messages, "\n")
endfunction

function! s:vimqf_func_define_linenum(srclines, line)
  let funcname = a:line =~ '<SNR>' ? matchstr(a:line, '<SNR>\d*_\zs.*\ze\s') : matchstr(a:line, 'function\s\zs\d*\ze\s')
  return exists('g:__func_lnums__') ? get(g:__func_lnums__, funcname, 0) : 0
endfunction

function! s:vimqf_hook_lines()
  return [
        \ 'function! s:vimqf_func_lnums(file)',
        \ '  let func_lnum = {}',
        \ '  let lines = readfile(a:file)',
        \ '  for i in range(0, len(lines)-1)',
        \ '    let l = lines[i]',
        \ '    let func_define = s:vimqf_func_define_line(matchstr(l, ''function!\s*\zs.*\ze(''))',
        \ '    if !empty(func_define)',
        \ '      let simplename = func_define =~ ''<SNR>''',
        \ '            \ ? matchstr(func_define, ''<SNR>\d*_\zs.*\ze('')',
        \ '            \ : matchstr(func_define, ''.*\s\zs\d*\ze('')',
        \ '      if !empty(simplename)',
        \ '        let func_lnum[simplename] = i + 1',
        \ '      endif',
        \ '    endif',
        \ '  endfor',
        \ '  return func_lnum',
        \ 'endfunction',
        \ 'function! s:vimqf_func_define_line(funcname)',
        \ '  let tempfile = tempname()',
        \ '  let save_vfile = &verbosefile',
        \ '  let &verbosefile = tempfile',
        \ '  try',
        \ '    silent! execute ''function '' . a:funcname',
        \ '  finally',
        \ '    if &verbosefile ==# tempfile',
        \ '      let &verbosefile = save_vfile',
        \ '    endif',
        \ '  endtry',
        \ '  let messages = ''''',
        \ '  if filereadable(tempfile)',
        \ '    let messages .= join(readfile(tempfile, ''b''), "\n")',
        \ '    call delete(tempfile)',
        \ '  endif',
        \ '  return split(messages, "\n")[0]',
        \ 'endfunction',
        \ 'let g:__func_lnums__ = s:vimqf_func_lnums(expand("%"))'
        \]
endfunction

command! -nargs=0 VimLint call s:vim_lint(expand('%:p'))
nnoremap <Space>l :<C-u>VimLint<CR>

課題

関数内に定義された辞書関数は解釈できない


ただ、ちょっと疲れたので他のことやろうかなという気になってる