Skip to content

Commit

Permalink
Adds standardrb linter (#2133)
Browse files Browse the repository at this point in the history
See: https://github.com/testdouble/standard

StandardRB is to RuboCop what StandardJS is to ESLint. This commit 
naively copies the RuboCop linter and fixer to point at the standardrb
executable. Any other adjustments are very minor (the only I can think 
of is that standardrb takes a `--fix` option instead of 
`--auto-correct`).

This raises a confusing point to me as both developer and a user: since
ale enables all linters by default, won't this run both RuboCop and 
StandardRB (the results of which will almost always be in conflict with
one another)? How does ale already solve for this for the similar case
of StandardJS and ESLint?
  • Loading branch information
searls authored and neersighted committed Dec 10, 2018
1 parent c899ff3 commit 2cfa09e
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 39 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -182,7 +182,7 @@ formatting.
| reStructuredText | [alex](https://github.com/wooorm/alex) !!, [proselint](http://proselint.com/), [redpen](http://redpen.cc/), [rstcheck](https://github.com/myint/rstcheck), [vale](https://github.com/ValeLint/vale), [write-good](https://github.com/btford/write-good) |
| Re:VIEW | [redpen](http://redpen.cc/) |
| RPM spec | [rpmlint](https://github.com/rpm-software-management/rpmlint) (disabled by default; see `:help ale-integration-spec`) |
| Ruby | [brakeman](http://brakemanscanner.org/) !!, [rails_best_practices](https://github.com/flyerhzm/rails_best_practices) !!, [reek](https://github.com/troessner/reek), [rubocop](https://github.com/bbatsov/rubocop), [ruby](https://www.ruby-lang.org), [rufo](https://github.com/ruby-formatter/rufo), [solargraph](https://solargraph.org) |
| Ruby | [brakeman](http://brakemanscanner.org/) !!, [rails_best_practices](https://github.com/flyerhzm/rails_best_practices) !!, [reek](https://github.com/troessner/reek), [rubocop](https://github.com/bbatsov/rubocop), [ruby](https://www.ruby-lang.org), [rufo](https://github.com/ruby-formatter/rufo), [solargraph](https://solargraph.org), [standardrb](https://github.com/testdouble/standard) |
| Rust | [cargo](https://github.com/rust-lang/cargo) !! (see `:help ale-integration-rust` for configuration instructions), [rls](https://github.com/rust-lang-nursery/rls), [rustc](https://www.rust-lang.org/), [rustfmt](https://github.com/rust-lang-nursery/rustfmt) |
| SASS | [sass-lint](https://www.npmjs.com/package/sass-lint), [stylelint](https://github.com/stylelint/stylelint) |
| SCSS | [prettier](https://github.com/prettier/prettier), [sass-lint](https://www.npmjs.com/package/sass-lint), [scss-lint](https://github.com/brigade/scss-lint), [stylelint](https://github.com/stylelint/stylelint) |
Expand Down
32 changes: 1 addition & 31 deletions ale_linters/ruby/rubocop.vim
Expand Up @@ -13,36 +13,6 @@ function! ale_linters#ruby#rubocop#GetCommand(buffer) abort
\ . ' --stdin ' . ale#Escape(expand('#' . a:buffer . ':p'))
endfunction

function! ale_linters#ruby#rubocop#Handle(buffer, lines) abort
try
let l:errors = json_decode(a:lines[0])
catch
return []
endtry

if !has_key(l:errors, 'summary')
\|| l:errors['summary']['offense_count'] == 0
\|| empty(l:errors['files'])
return []
endif

let l:output = []

for l:error in l:errors['files'][0]['offenses']
let l:start_col = l:error['location']['column'] + 0
call add(l:output, {
\ 'lnum': l:error['location']['line'] + 0,
\ 'col': l:start_col,
\ 'end_col': l:start_col + l:error['location']['length'] - 1,
\ 'code': l:error['cop_name'],
\ 'text': l:error['message'],
\ 'type': ale_linters#ruby#rubocop#GetType(l:error['severity']),
\})
endfor

return l:output
endfunction

function! ale_linters#ruby#rubocop#GetType(severity) abort
if a:severity is? 'convention'
\|| a:severity is? 'warning'
Expand All @@ -57,5 +27,5 @@ call ale#linter#Define('ruby', {
\ 'name': 'rubocop',
\ 'executable_callback': ale#VarFunc('ruby_rubocop_executable'),
\ 'command_callback': 'ale_linters#ruby#rubocop#GetCommand',
\ 'callback': 'ale_linters#ruby#rubocop#Handle',
\ 'callback': 'ale#ruby#HandleRubocopOutput',
\})
23 changes: 23 additions & 0 deletions ale_linters/ruby/standardrb.vim
@@ -0,0 +1,23 @@
" Author: Justin Searls https://github.com/searls, ynonp - https://github.com/ynonp, Eddie Lebow https://github.com/elebow
" based on the ale rubocop linter
" Description: StandardRB - Ruby Style Guide, with linter & automatic code fixer

call ale#Set('ruby_standardrb_executable', 'standardrb')
call ale#Set('ruby_standardrb_options', '')

function! ale_linters#ruby#standardrb#GetCommand(buffer) abort
let l:executable = ale#Var(a:buffer, 'ruby_standardrb_executable')

return ale#handlers#ruby#EscapeExecutable(l:executable, 'standardrb')
\ . ' --format json --force-exclusion '
\ . ale#Var(a:buffer, 'ruby_standardrb_options')
\ . ' --stdin ' . ale#Escape(expand('#' . a:buffer . ':p'))
endfunction

" standardrb is based on RuboCop so the callback is the same
call ale#linter#Define('ruby', {
\ 'name': 'standardrb',
\ 'executable_callback': ale#VarFunc('ruby_standardrb_executable'),
\ 'command_callback': 'ale_linters#ruby#standardrb#GetCommand',
\ 'callback': 'ale#ruby#HandleRubocopOutput',
\})
5 changes: 5 additions & 0 deletions autoload/ale/fix/registry.vim
Expand Up @@ -115,6 +115,11 @@ let s:default_registry = {
\ 'suggested_filetypes': ['javascript'],
\ 'description': 'Fix JavaScript files using standard --fix',
\ },
\ 'standardrb': {
\ 'function': 'ale#fixers#standardrb#Fix',
\ 'suggested_filetypes': ['ruby'],
\ 'description': 'Fix ruby files with standardrb --fix',
\ },
\ 'stylelint': {
\ 'function': 'ale#fixers#stylelint#Fix',
\ 'suggested_filetypes': ['css', 'sass', 'scss', 'stylus'],
Expand Down
23 changes: 23 additions & 0 deletions autoload/ale/fixers/standardrb.vim
@@ -0,0 +1,23 @@
" Author: Justin Searls - https://github.com/searls
" Description: Fix Ruby files with StandardRB.

call ale#Set('ruby_standardrb_options', '')
call ale#Set('ruby_standardrb_executable', 'standardrb')

function! ale#fixers#standardrb#GetCommand(buffer) abort
let l:executable = ale#Var(a:buffer, 'ruby_standardrb_executable')
let l:config = ale#path#FindNearestFile(a:buffer, '.standard.yml')
let l:options = ale#Var(a:buffer, 'ruby_standardrb_options')

return ale#handlers#ruby#EscapeExecutable(l:executable, 'standardrb')
\ . (!empty(l:config) ? ' --config ' . ale#Escape(l:config) : '')
\ . (!empty(l:options) ? ' ' . l:options : '')
\ . ' --fix --force-exclusion %t'
endfunction

function! ale#fixers#standardrb#Fix(buffer) abort
return {
\ 'command': ale#fixers#standardrb#GetCommand(a:buffer),
\ 'read_temporary_file': 1,
\}
endfunction
32 changes: 32 additions & 0 deletions autoload/ale/ruby.vim
Expand Up @@ -42,3 +42,35 @@ function! ale#ruby#FindProjectRoot(buffer) abort

return ''
endfunction

" Handle output from rubocop and linters that depend on it (e.b. standardrb)
function! ale#ruby#HandleRubocopOutput(buffer, lines) abort
try
let l:errors = json_decode(a:lines[0])
catch
return []
endtry

if !has_key(l:errors, 'summary')
\|| l:errors['summary']['offense_count'] == 0
\|| empty(l:errors['files'])
return []
endif

let l:output = []

for l:error in l:errors['files'][0]['offenses']
let l:start_col = l:error['location']['column'] + 0
call add(l:output, {
\ 'lnum': l:error['location']['line'] + 0,
\ 'col': l:start_col,
\ 'end_col': l:start_col + l:error['location']['length'] - 1,
\ 'code': l:error['cop_name'],
\ 'text': l:error['message'],
\ 'type': ale_linters#ruby#rubocop#GetType(l:error['severity']),
\})
endfor

return l:output
endfunction

22 changes: 22 additions & 0 deletions doc/ale-ruby.txt
Expand Up @@ -129,5 +129,27 @@ g:ale_ruby_solargraph_executable *g:ale_ruby_solargraph_executable*
from binstubs or a bundle.


===============================================================================
standardrb *ale-ruby-standardrb*

g:ale_ruby_standardrb_executable *g:ale_ruby_standardrb_executable*
*b:ale_ruby_standardrb_executable*
Type: String
Default: `'standardrb'`

Override the invoked standardrb binary. Set this to `'bundle'` to invoke
`'bundle` `exec` standardrb'.


g:ale_ruby_standardrb_options *g:ale_ruby_standardrb_options*
*b:ale_ruby_standardrb_options*
Type: |String|
Default: `''`

This variable can be change to modify flags given to standardrb.


===============================================================================

===============================================================================
vim:tw=78:ts=2:sts=2:sw=2:ft=help:norl:
3 changes: 2 additions & 1 deletion doc/ale.txt
Expand Up @@ -285,6 +285,7 @@ CONTENTS *ale-contents*
ruby................................|ale-ruby-ruby|
rufo................................|ale-ruby-rufo|
solargraph..........................|ale-ruby-solargraph|
standardrb..........................|ale-ruby-standardrb|
rust..................................|ale-rust-options|
cargo...............................|ale-rust-cargo|
rls.................................|ale-rust-rls|
Expand Down Expand Up @@ -486,7 +487,7 @@ Notes:
* reStructuredText: `alex`!!, `proselint`, `redpen`, `rstcheck`, `vale`, `write-good`
* Re:VIEW: `redpen`
* RPM spec: `rpmlint`
* Ruby: `brakeman`, `rails_best_practices`!!, `reek`, `rubocop`, `ruby`, `rufo`, `solargraph`
* Ruby: `brakeman`, `rails_best_practices`!!, `reek`, `rubocop`, `ruby`, `rufo`, `solargraph`, `standardrb`
* Rust: `cargo`!!, `rls`, `rustc` (see |ale-integration-rust|), `rustfmt`
* SASS: `sass-lint`, `stylelint`
* SCSS: `prettier`, `sass-lint`, `scss-lint`, `stylelint`
Expand Down
Empty file.
29 changes: 29 additions & 0 deletions test/command_callback/test_standardrb_command_callback.vader
@@ -0,0 +1,29 @@
Before:
call ale#assert#SetUpLinterTest('ruby', 'standardrb')
call ale#test#SetFilename('dummy.rb')

let g:ale_ruby_standardrb_executable = 'standardrb'
let g:ale_ruby_standardrb_options = ''

After:
call ale#assert#TearDownLinterTest()

Execute(Executable should default to standardrb):
AssertLinter 'standardrb', ale#Escape('standardrb')
\ . ' --format json --force-exclusion --stdin '
\ . ale#Escape(ale#path#Simplify(g:dir . '/dummy.rb'))

Execute(Should be able to set a custom executable):
let g:ale_ruby_standardrb_executable = 'bin/standardrb'

AssertLinter 'bin/standardrb' , ale#Escape('bin/standardrb')
\ . ' --format json --force-exclusion --stdin '
\ . ale#Escape(ale#path#Simplify(g:dir . '/dummy.rb'))

Execute(Setting bundle appends 'exec standardrb'):
let g:ale_ruby_standardrb_executable = 'path to/bundle'

AssertLinter 'path to/bundle', ale#Escape('path to/bundle')
\ . ' exec standardrb'
\ . ' --format json --force-exclusion --stdin '
\ . ale#Escape(ale#path#Simplify(g:dir . '/dummy.rb'))
54 changes: 54 additions & 0 deletions test/fixers/test_standardrb_fixer_callback.vader
@@ -0,0 +1,54 @@
Before:
Save g:ale_ruby_standardrb_executable
Save g:ale_ruby_standardrb_options

" Use an invalid global executable, so we don't match it.
let g:ale_ruby_standardrb_executable = 'xxxinvalid'
let g:ale_ruby_standardrb_options = ''

call ale#test#SetDirectory('/testplugin/test/fixers')
silent cd ..
silent cd command_callback
let g:dir = getcwd()

After:
Restore

call ale#test#RestoreDirectory()

Execute(The standardrb callback should return the correct default values):
call ale#test#SetFilename('ruby_paths/dummy.rb')

AssertEqual
\ {
\ 'read_temporary_file': 1,
\ 'command': ale#Escape(g:ale_ruby_standardrb_executable)
\ . ' --fix --force-exclusion %t',
\ },
\ ale#fixers#standardrb#Fix(bufnr(''))

Execute(The standardrb callback should include configuration files):
call ale#test#SetFilename('ruby_paths/with_config/dummy.rb')

AssertEqual
\ {
\ 'read_temporary_file': 1,
\ 'command': ale#Escape(g:ale_ruby_standardrb_executable)
\ . ' --config ' . ale#Escape(ale#path#Simplify(g:dir . '/ruby_paths/with_config/.standard.yml'))
\ . ' --fix --force-exclusion %t',
\ },
\ ale#fixers#standardrb#Fix(bufnr(''))

Execute(The standardrb callback should include custom rubocop options):
let g:ale_ruby_standardrb_options = '--except Lint/Debugger'
call ale#test#SetFilename('ruby_paths/with_config/dummy.rb')

AssertEqual
\ {
\ 'read_temporary_file': 1,
\ 'command': ale#Escape(g:ale_ruby_standardrb_executable)
\ . ' --config ' . ale#Escape(ale#path#Simplify(g:dir . '/ruby_paths/with_config/.standard.yml'))
\ . ' --except Lint/Debugger'
\ . ' --fix --force-exclusion %t',
\ },
\ ale#fixers#standardrb#Fix(bufnr(''))
12 changes: 6 additions & 6 deletions test/handler/test_rubocop_handler.vader
Expand Up @@ -41,21 +41,21 @@ Execute(The rubocop handler should parse lines correctly):
\ 'type': 'E',
\ },
\ ],
\ ale_linters#ruby#rubocop#Handle(347, [
\ ale#ruby#HandleRubocopOutput(347, [
\ '{"metadata":{"rubocop_version":"0.47.1","ruby_engine":"ruby","ruby_version":"2.1.5","ruby_patchlevel":"273","ruby_platform":"x86_64-linux-gnu"},"files":[{"path":"my_great_file.rb","offenses":[{"severity":"convention","message":"Prefer single-quoted strings...","cop_name":"Style/SomeCop","corrected":false,"location":{"line":83,"column":29,"length":7}},{"severity":"fatal","message":"Some error","cop_name":"Style/SomeOtherCop","corrected":false,"location":{"line":12,"column":2,"length":1}},{"severity":"warning","message":"Regular warning","cop_name":"Style/WarningCop","corrected":false,"location":{"line":10,"column":5,"length":8}},{"severity":"error","message":"Another error","cop_name":"Style/SpaceBeforeBlockBraces","corrected":false,"location":{"line":11,"column":1,"length":1}}]}],"summary":{"offense_count":4,"target_file_count":1,"inspected_file_count":1}}'
\ ])

Execute(The rubocop handler should handle when files are checked and no offenses are found):
AssertEqual
\ [],
\ ale_linters#ruby#rubocop#Handle(347, [
\ ale#ruby#HandleRubocopOutput(347, [
\ '{"metadata":{"rubocop_version":"0.47.1","ruby_engine":"ruby","ruby_version":"2.1.5","ruby_patchlevel":"273","ruby_platform":"x86_64-linux-gnu"},"files":[{"path":"my_great_file.rb","offenses":[]}],"summary":{"offense_count":0,"target_file_count":1,"inspected_file_count":1}}'
\ ])

Execute(The rubocop handler should handle when no files are checked):
AssertEqual
\ [],
\ ale_linters#ruby#rubocop#Handle(347, [
\ ale#ruby#HandleRubocopOutput(347, [
\ '{"metadata":{"rubocop_version":"0.47.1","ruby_engine":"ruby","ruby_version":"2.1.5","ruby_patchlevel":"273","ruby_platform":"x86_64-linux-gnu"},"files":[],"summary":{"offense_count":0,"target_file_count":0,"inspected_file_count":0}}'
\ ])

Expand All @@ -66,11 +66,11 @@ Execute(The rubocop handler should handle output without any errors):

AssertEqual
\ [],
\ ale_linters#ruby#rubocop#Handle(347, g:lines)
\ ale#ruby#HandleRubocopOutput(347, g:lines)
\
AssertEqual
\ [],
\ ale_linters#ruby#rubocop#Handle(347, ['{}'])
\ ale#ruby#HandleRubocopOutput(347, ['{}'])
AssertEqual
\ [],
\ ale_linters#ruby#rubocop#Handle(347, [])
\ ale#ruby#HandleRubocopOutput(347, [])

0 comments on commit 2cfa09e

Please sign in to comment.