This is a continuation of my buffer pre-write hook series. Check out the previous gist to follow the progression.
Have you ever tried running a file only to be stopped by "<<<<<<<"?
Wouldn't it be nice to be able to automatically run your code through pyflakes before actually saving it? Maybe even raise an error for showstopping bugs in your code?
Tired of being yelled at by angry coworkers for accidentally leaving trailing whitespace in your files?
In my VIM status bar, I like to have indicators for things like trailing whitespace, version control conflict markers, and pyflakes errors. These are issues that should normally be resolved before checking in code. Being human, I sometimes forget to handle these issues. Fortunately, I have a computer that never forgets.
This gist hopes to demonstrate how to fix these issues by forcing a pre-write hook to make sure your code is clean before saving it. (This is basically an extension of my gist about handling whitespace. I realized whitespace wasn't the only thing that should block a save.)
From a high level, what we want to do is run through a serious of checks and raise exceptions if the checks fail. Should be simple and for the most part, it is. To begin, add the following to your vimrc
function! RaiseExceptionForUnresolvedErrors()
endfunction
autocmd BufWritePre * call RaiseExceptionForUnresolvedErrors()
This should be pretty self-explanatory. We're defining a function called RaiseExceptionForUnresolvedErrors and calling it before any buffer is written. From here, we will add the various checks between the function/endfunction lines.
Checking for version control markers is pretty simple. We just need to come up with a regex to match the markers. I find that
^[<=>]{7}( .*|$)
works pretty well. Next, we wrap it in a search function and add it to RaiseExceptionForUnresolvedErrors.
function! RaiseExceptionForUnresolvedErrors()
if search('\v^[<=>]{7}( .*|$)', 'nw') != 0
throw 'Found unresolved conflicts'
endif
...
Again, this should be pretty straightforward. I've added \v to the regex pattern to trigger "very magic" mode and passed the flags 'nw' to the search function. Check the VIM help pages for more information on those.
Similar to the check for version control markers, the method here will be to write a regex to match trailing whitespace.
\s+$
Like before, I will wrap the regex in a search function and throw an error on failure.
function! RaiseExceptionForUnresolvedIssues()
...
if search('\s\+$', 'nw') != 0
throw 'Found trailing whitespace'
endif
...
Notice, I don't use a "very magic" search pattern here. It's not worth the extra keystroke :-)
I like using the syntastic plugin with VIM. This plugin displays any errors from pyflakes for the current file. However, it can only update the errors AFTER the file has been written. My goal is to prevent writing a bad file in the first place. This ends up being slightly trickier than checking for the other issues but it should still be possible.
My method here is to make a copy of the current buffer and pipe it into pyflakes through stdin. I overwrite the temporary buffer with the result from pyflakes and begin checking for the errors that I'm interested in. If you're not interested in the "magic" to make that happen, skip below.
let s:file_name = expand('%:t')
%yank p
new
0put p
$,$d
The first thing I do is grab the name of the current file. That'll be useful in the next step. Next, I yank the entire buffer into register p, create a new split, and paste register p into it. The last line here is a little bit of clean up to trim the blank line from the end of the new buffer.
%!pyflakes
exe '%s/<stdin>/' . s:file_name . '/e'
unlet! s:file_name
The first line takes our newly created buffer and pipes it into pyflakes. Because we're technically sending the buffer to pyflakes through stdin, pyflakes doesn't actually know the name of the file when reporting errors. So, the second line replaces any references to stdin with the file name we saved in the previous step.
Pyflakes can search for all sorts of errors. However, I'm only really interested in certain ones. In particular, undefined name, unexpected indent, and invalid syntax should be showstoppers. There's nothing stopping you from adding or removing check to suit your style.
The basic form I'm using is search for error name. If we find that error, store the line number, get the message from that line, destroy the temporary buffer, and raise the error message.
This could look like the following:
let s:un_res = search('undefined name', 'nw')
if s:un_res != 0
let s:message = 'Syntax error! ' . getline(s:un_res)
bd!
throw s:message
endif
let s:ui_res = search('unexpected indent', 'nw')
if s:ui_res != 0
let s:message = 'Syntax error! ' . getline(s:ui_res)
bd!
throw s:message
endif
let s:is_res = search('invalid syntax', 'nw')
if s:is_res != 0
let s:message = 'Syntax error! ' . getline(s:is_res)
bd!
throw s:message
endif
Assuming, we've made it through all of that, we can safely destroy our temporary buffer with one final
bd!
Obviously, it doesn't really make sense to run non-python files through pyflakes, so I wrap everything in a filetype check. Also, I've prepended the 'silent' keyword to a couple of commands. VIM can be chatty sometimes so it just makes everything cleaner.
Thus, we end up with the following:
if &filetype == 'python'
let s:file_name = expand('%:t')
silent %yank p
new
silent 0put p
silent $,$d
silent %!pyflakes
silent exe '%s/<stdin>/' . s:file_name . '/e'
unlet! s:file_name
let s:un_res = search('undefined name', 'nw')
if s:un_res != 0
let s:message = 'Syntax error! ' . getline(s:un_res)
bd!
throw s:message
endif
let s:ui_res = search('unexpected indent', 'nw')
if s:ui_res != 0
let s:message = 'Syntax error! ' . getline(s:ui_res)
bd!
throw s:message
endif
let s:is_res = search('invalid syntax', 'nw')
if s:is_res != 0
let s:message = 'Syntax error! ' . getline(s:is_res)
bd!
throw s:message
endif
bd!
endif
Altogether my addition to my vimrc looks like this
function! RaiseExceptionForUnresolvedErrors()
if search('\v^[<=>]{7}( .*|$)', 'nw') != 0
throw 'Found unresolved conflicts'
endif
if search('\s\+$', 'nw') != 0
throw 'Found trailing whitespace'
endif
if &filetype == 'python'
let s:file_name = expand('%:t')
silent %yank p
new
silent 0put p
silent $,$d
silent %!pyflakes
silent exe '%s/<stdin>/' . s:file_name . '/e'
unlet! s:file_name
let s:un_res = search('undefined name', 'nw')
if s:un_res != 0
let s:message = 'Syntax error! ' . getline(s:un_res)
bd!
throw s:message
endif
let s:ui_res = search('unexpected indent', 'nw')
if s:ui_res != 0
let s:message = 'Syntax error! ' . getline(s:ui_res)
bd!
throw s:message
endif
let s:is_res = search('invalid syntax', 'nw')
if s:is_res != 0
let s:message = 'Syntax error! ' . getline(s:is_res)
bd!
throw s:message
endif
bd!
endif
endfunction
autocmd BufWritePre * call RaiseExceptionForUnresolvedErrors()
An exception will be thrown when attempting to write any file containing unresolved conflict markers, trailing whitespace, or pyflakes errors. One of the reasons that I really like this method is that if you want to force the write to succeed anyway, you still have that ability. This can be accomplished by issuing the write command with noautocmd.
:noautocmd w
Or,
:noa w
Please check out the other gists in the series
Can't star this enough ;)