summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLoic Nageleisen <loic.nageleisen@gmail.com>2024-06-25 10:17:27 +0200
committerGitHub <noreply@github.com>2024-06-25 17:17:27 +0900
commite09520e2d7d7af09e82da7e845541b67d15d8d1d (patch)
treee7ada9cee19223205f1886b91ff107a572f026e9
parentf4bc3c2711a004daf749cdba24e20144896f441d (diff)
downloadale-e09520e2d7d7af09e82da7e845541b67d15d8d1d.zip
Add Ruby linter with Steep (#4671)
* Add Ruby linter with Steep Fixes #3254 * Run steep instead of using language server LSP presents a few issues and this works around those. * Work around Steep path issue See https://github.com/soutaro/steep/pull/975 * Add simple tests for steep * Add steep to supported tools * Pass linter * Add a comment regarding Steep's column counting * Make lnum an integer * Add Steep handler test * Fix separator for Windows * Escape Windows path separators for substitute() * Use ALEInfo (I) group * Use fnameescape instead of quotes * Skip linting for files not under steep root * Add and pass tests covering proper steep root lookup * Fix separator discrepancy * Use strict operators (match case) * Fix ordering * Use `is#` instead of `==#`
-rw-r--r--ale_linters/ruby/steep.vim172
-rw-r--r--doc/ale-supported-languages-and-tools.txt1
-rw-r--r--supported-tools.md1
-rw-r--r--test/handler/test_steep_handler.vader100
-rw-r--r--test/linter/test_ruby_steep.vader69
-rw-r--r--test/test-files/ruby/nested/dummy.rb0
-rw-r--r--test/test-files/ruby/nested/foo/Steepfile0
-rw-r--r--test/test-files/ruby/nested/foo/dummy.rb0
-rw-r--r--test/test-files/ruby/nested/foo/one/dummy.rb0
-rw-r--r--test/test-files/ruby/nested/foo/two/Steepfile0
-rw-r--r--test/test-files/ruby/nested/foo/two/dummmy.rb0
-rw-r--r--test/test-files/ruby/nested/foo/two/three/dummy.rb0
12 files changed, 343 insertions, 0 deletions
diff --git a/ale_linters/ruby/steep.vim b/ale_linters/ruby/steep.vim
new file mode 100644
index 00000000..4fd52620
--- /dev/null
+++ b/ale_linters/ruby/steep.vim
@@ -0,0 +1,172 @@
+call ale#Set('ruby_steep_executable', 'steep')
+call ale#Set('ruby_steep_options', '')
+
+" Find the nearest dir containing a Steepfile
+function! ale_linters#ruby#steep#FindRoot(buffer) abort
+ for l:name in ['Steepfile']
+ let l:dir = fnamemodify(
+ \ ale#path#FindNearestFile(a:buffer, l:name),
+ \ ':h'
+ \)
+
+ if l:dir isnot# '.' && isdirectory(l:dir)
+ return l:dir
+ endif
+ endfor
+
+ return ''
+endfunction
+
+" Rename path relative to root
+function! ale_linters#ruby#steep#RelativeToRoot(buffer, path) abort
+ let l:separator = has('win32') ? '\' : '/'
+ let l:steep_root = ale_linters#ruby#steep#FindRoot(a:buffer)
+
+ " path isn't under root
+ if l:steep_root is# ''
+ return ''
+ endif
+
+ let l:steep_root_prefix = l:steep_root . l:separator
+
+ " win32 path separators get interpreted by substitute, escape them
+ if has('win32')
+ let l:steep_root_pat = substitute(l:steep_root_prefix, '\\', '\\\\', 'g')
+ else
+ let l:steep_root_pat = l:steep_root_prefix
+ endif
+
+ return substitute(a:path, l:steep_root_pat, '', '')
+endfunction
+
+function! ale_linters#ruby#steep#GetCommand(buffer) abort
+ let l:executable = ale#Var(a:buffer, 'ruby_steep_executable')
+
+ " steep check needs to apply some config from the file path so:
+ " - steep check can't use stdin (no path)
+ " - steep check can't use %t (path outside of project)
+ " => we can only use %s
+
+ " somehow :ALEInfo shows that ALE still appends '< %t' to the command
+ " => luckily steep check ignores stdin
+
+ " somehow steep has a problem with absolute path to file but a path
+ " relative to Steepfile directory works:
+ " see https://github.com/soutaro/steep/pull/975
+ " => change to Steepfile directory and remove leading path
+
+ let l:buffer_filename = fnamemodify(bufname(a:buffer), ':p')
+ let l:buffer_filename = fnameescape(l:buffer_filename)
+
+ let l:relative = ale_linters#ruby#steep#RelativeToRoot(a:buffer, l:buffer_filename)
+
+ " if file is not under steep root, steep can't type check
+ if l:relative is# ''
+ " don't execute
+ return ''
+ endif
+
+ return ale#ruby#EscapeExecutable(l:executable, 'steep')
+ \ . ' check '
+ \ . ale#Var(a:buffer, 'ruby_steep_options')
+ \ . ' ' . fnameescape(l:relative)
+endfunction
+
+function! ale_linters#ruby#steep#GetType(severity) abort
+ if a:severity is? 'information'
+ \|| a:severity is? 'hint'
+ return 'I'
+ endif
+
+ if a:severity is? 'warning'
+ return 'W'
+ endif
+
+ return 'E'
+endfunction
+
+" Handle output from steep
+function! ale_linters#ruby#steep#HandleOutput(buffer, lines) abort
+ let l:output = []
+
+ let l:in = 0
+ let l:item = {}
+
+ for l:line in a:lines
+ " Look for first line of a message block
+ " If not in-message (l:in == 0) that's expected
+ " If in-message (l:in > 0) that's less expected but let's recover
+ let l:match = matchlist(l:line, '^\([^:]*\):\([0-9]*\):\([0-9]*\): \[\([^]]*\)\] \(.*\)')
+
+ if len(l:match) > 0
+ " Something is lingering: recover by pushing what is there
+ if len(l:item) > 0
+ call add(l:output, l:item)
+ let l:item = {}
+ endif
+
+ let l:filename = l:match[1]
+
+ " Steep's reported column is offset by 1 (zero-indexed?)
+ let l:item = {
+ \ 'lnum': l:match[2] + 0,
+ \ 'col': l:match[3] + 1,
+ \ 'type': ale_linters#ruby#steep#GetType(l:match[4]),
+ \ 'text': l:match[5],
+ \}
+
+ " Done with this line, mark being in-message and go on with next line
+ let l:in = 1
+ continue
+ endif
+
+ " We're past the first line of a message block
+ if l:in > 0
+ " Look for code in subsequent lines of the message block
+ if l:line =~# '^│ Diagnostic ID:'
+ let l:match = matchlist(l:line, '^│ Diagnostic ID: \(.*\)')
+
+ if len(l:match) > 0
+ let l:item.code = l:match[1]
+ endif
+
+ " Done with the line
+ continue
+ endif
+
+ " Look for last line of the message block
+ if l:line =~# '^└'
+ " Done with the line, mark looking for underline and go on with the next line
+ let l:in = 2
+ continue
+ endif
+
+ " Look for underline right after last line
+ if l:in == 2
+ let l:match = matchlist(l:line, '\([~][~]*\)')
+
+ if len(l:match) > 0
+ let l:item.end_col = l:item['col'] + len(l:match[1]) - 1
+ endif
+
+ call add(l:output, l:item)
+
+ " Done with the line, mark looking for first line and go on with the next line
+ let l:in = 0
+ let l:item = {}
+ continue
+ endif
+ endif
+ endfor
+
+ return l:output
+endfunction
+
+call ale#linter#Define('ruby', {
+\ 'name': 'steep',
+\ 'executable': {b -> ale#Var(b, 'ruby_steep_executable')},
+\ 'language': 'ruby',
+\ 'command': function('ale_linters#ruby#steep#GetCommand'),
+\ 'project_root': function('ale_linters#ruby#steep#FindRoot'),
+\ 'callback': 'ale_linters#ruby#steep#HandleOutput',
+\})
diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt
index 2e4b4ab0..505cac86 100644
--- a/doc/ale-supported-languages-and-tools.txt
+++ b/doc/ale-supported-languages-and-tools.txt
@@ -572,6 +572,7 @@ Notes:
* `solargraph`
* `sorbet`
* `standardrb`
+ * `steep`
* `syntax_tree`
* Rust
* `cargo`!!
diff --git a/supported-tools.md b/supported-tools.md
index 56a5a6c9..3d687614 100644
--- a/supported-tools.md
+++ b/supported-tools.md
@@ -581,6 +581,7 @@ formatting.
* [solargraph](https://solargraph.org)
* [sorbet](https://github.com/sorbet/sorbet)
* [standardrb](https://github.com/testdouble/standard)
+ * [steep](https://github.com/soutaro/steep)
* [syntax_tree](https://github.com/ruby-syntax-tree/syntax_tree)
* Rust
* [cargo](https://github.com/rust-lang/cargo) :floppy_disk: (see `:help ale-integration-rust` for configuration instructions)
diff --git a/test/handler/test_steep_handler.vader b/test/handler/test_steep_handler.vader
new file mode 100644
index 00000000..98875229
--- /dev/null
+++ b/test/handler/test_steep_handler.vader
@@ -0,0 +1,100 @@
+Before:
+ runtime ale_linters/ruby/steep.vim
+
+After:
+ call ale#linter#Reset()
+
+Execute(The steep handler should parse lines correctly):
+ AssertEqual
+ \ [
+ \ {
+ \ 'lnum': 400,
+ \ 'col': 18,
+ \ 'end_col': 45,
+ \ 'text': 'Method parameters are incompatible with declaration `(untyped, untyped, *untyped, **untyped) { () -> untyped } -> untyped`',
+ \ 'code': 'Ruby::MethodArityMismatch',
+ \ 'type': 'E',
+ \ },
+ \ {
+ \ 'lnum': 20,
+ \ 'col': 9,
+ \ 'end_col': 17,
+ \ 'text': 'Cannot find implementation of method `::Frobz::FooBarBaz#method_name`',
+ \ 'code': 'Ruby::MethodDefinitionMissing',
+ \ 'type': 'W',
+ \ },
+ \ {
+ \ 'lnum': 30,
+ \ 'col': 9,
+ \ 'end_col': 17,
+ \ 'text': 'Cannot find implementation of method `::Frobz::FooBarBaz#method_name`',
+ \ 'code': 'Ruby::MethodDefinitionMissing',
+ \ 'type': 'I',
+ \ },
+ \ {
+ \ 'lnum': 40,
+ \ 'col': 9,
+ \ 'end_col': 17,
+ \ 'text': 'Cannot find implementation of method `::Frobz::FooBarBaz#method_name`',
+ \ 'code': 'Ruby::MethodDefinitionMissing',
+ \ 'type': 'I',
+ \ },
+ \ ],
+ \ ale_linters#ruby#steep#HandleOutput(347, [
+ \ '# Type checking files:',
+ \ '',
+ \ '...............................................................................................................................F..........F.F...F.',
+ \ '',
+ \ 'lib/frobz/foobar_baz.rb:400:17: [error] Method parameters are incompatible with declaration `(untyped, untyped, *untyped, **untyped) { () -> untyped } -> untyped`',
+ \ '│ Diagnostic ID: Ruby::MethodArityMismatch',
+ \ '│',
+ \ '└ def frobz(obj, suffix, *args, &block)',
+ \ ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
+ \ '',
+ \ 'lib/frobz/foobar_baz.rb:20:8: [warning] Cannot find implementation of method `::Frobz::FooBarBaz#method_name`',
+ \ '│ Diagnostic ID: Ruby::MethodDefinitionMissing',
+ \ '│',
+ \ '└ class FooBarBaz',
+ \ ' ~~~~~~~~~',
+ \ '',
+ \ 'lib/frobz/foobar_baz.rb:30:8: [information] Cannot find implementation of method `::Frobz::FooBarBaz#method_name`',
+ \ '│ Diagnostic ID: Ruby::MethodDefinitionMissing',
+ \ '│',
+ \ '└ class FooBarBaz',
+ \ ' ~~~~~~~~~',
+ \ '',
+ \ 'lib/frobz/foobar_baz.rb:40:8: [hint] Cannot find implementation of method `::Frobz::FooBarBaz#method_name`',
+ \ '│ Diagnostic ID: Ruby::MethodDefinitionMissing',
+ \ '│',
+ \ '└ class FooBarBaz',
+ \ ' ~~~~~~~~~',
+ \ '',
+ \ 'Detected 4 problems from 1 file',
+ \ ])
+
+Execute(The steep handler should handle when files are checked and no offenses are found):
+ AssertEqual
+ \ [],
+ \ ale_linters#ruby#steep#HandleOutput(347, [
+ \ '# Type checking files:',
+ \ '',
+ \ '.............................................................................................................................................',
+ \ '',
+ \ 'No type error detected. 🧉',
+ \ ])
+
+Execute(The steep handler should handle when no files are checked):
+ AssertEqual
+ \ [],
+ \ ale_linters#ruby#steep#HandleOutput(347, [
+ \ '# Type checking files:',
+ \ '',
+ \ '',
+ \ '',
+ \ 'No type error detected. 🧉',
+ \ ])
+
+Execute(The steep handler should handle empty output):
+ AssertEqual [], ale_linters#ruby#steep#HandleOutput(347, [''])
+ AssertEqual [], ale_linters#ruby#steep#HandleOutput(347, [])
+
diff --git a/test/linter/test_ruby_steep.vader b/test/linter/test_ruby_steep.vader
new file mode 100644
index 00000000..59ab1e73
--- /dev/null
+++ b/test/linter/test_ruby_steep.vader
@@ -0,0 +1,69 @@
+" Author: Loic Nageleisen <https://github.com/lloeki>
+" Description: Tests for steep linter.
+Before:
+ call ale#assert#SetUpLinterTest('ruby', 'steep')
+
+ let g:ale_ruby_steep_executable = 'steep'
+
+After:
+ call ale#assert#TearDownLinterTest()
+
+Execute(Executable should default to steep):
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/dummy.rb')
+ AssertLinter 'steep', ale#Escape('steep')
+ \ . ' check '
+ \ . ' dummy.rb'
+
+Execute(Should be able to set a custom executable):
+ let g:ale_ruby_steep_executable = 'bin/steep'
+
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/dummy.rb')
+ AssertLinter 'bin/steep' , ale#Escape('bin/steep')
+ \ . ' check '
+ \ . ' dummy.rb'
+
+Execute(Setting bundle appends 'exec steep'):
+ let g:ale_ruby_steep_executable = 'path to/bundle'
+
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/dummy.rb')
+ AssertLinter 'path to/bundle', ale#Escape('path to/bundle')
+ \ . ' exec steep'
+ \ . ' check '
+ \ . ' dummy.rb'
+
+Execute(should accept options):
+ let g:ale_ruby_steep_options = '--severity-level=hint'
+
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/dummy.rb')
+ AssertLinter 'steep', ale#Escape('steep')
+ \ . ' check'
+ \ . ' --severity-level=hint'
+ \ . ' dummy.rb'
+
+Execute(Should not lint files out of steep root):
+ call ale#test#SetFilename('../test-files/ruby/nested/dummy.rb')
+ AssertLinter 'steep', ''
+
+Execute(Should lint files at top steep root):
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/dummy.rb')
+ AssertLinter 'steep', ale#Escape('steep')
+ \ . ' check '
+ \ . ' dummy.rb'
+
+Execute(Should lint files below top steep root):
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/one/dummy.rb')
+ AssertLinter 'steep', ale#Escape('steep')
+ \ . ' check '
+ \ . ' one' . (has('win32') ? '\' : '/') . 'dummy.rb'
+
+Execute(Should lint files at nested steep root):
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/two/dummy.rb')
+ AssertLinter 'steep', ale#Escape('steep')
+ \ . ' check '
+ \ . ' dummy.rb'
+
+Execute(Should lint files below nested steep root):
+ call ale#test#SetFilename('../test-files/ruby/nested/foo/two/three/dummy.rb')
+ AssertLinter 'steep', ale#Escape('steep')
+ \ . ' check '
+ \ . ' three' . (has('win32') ? '\' : '/') . 'dummy.rb'
diff --git a/test/test-files/ruby/nested/dummy.rb b/test/test-files/ruby/nested/dummy.rb
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/test-files/ruby/nested/dummy.rb
diff --git a/test/test-files/ruby/nested/foo/Steepfile b/test/test-files/ruby/nested/foo/Steepfile
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/test-files/ruby/nested/foo/Steepfile
diff --git a/test/test-files/ruby/nested/foo/dummy.rb b/test/test-files/ruby/nested/foo/dummy.rb
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/test-files/ruby/nested/foo/dummy.rb
diff --git a/test/test-files/ruby/nested/foo/one/dummy.rb b/test/test-files/ruby/nested/foo/one/dummy.rb
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/test-files/ruby/nested/foo/one/dummy.rb
diff --git a/test/test-files/ruby/nested/foo/two/Steepfile b/test/test-files/ruby/nested/foo/two/Steepfile
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/test-files/ruby/nested/foo/two/Steepfile
diff --git a/test/test-files/ruby/nested/foo/two/dummmy.rb b/test/test-files/ruby/nested/foo/two/dummmy.rb
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/test-files/ruby/nested/foo/two/dummmy.rb
diff --git a/test/test-files/ruby/nested/foo/two/three/dummy.rb b/test/test-files/ruby/nested/foo/two/three/dummy.rb
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/test-files/ruby/nested/foo/two/three/dummy.rb