1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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',
\})
|