]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/handlers/eslint.vim

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/ale/' content from commit 22185c4c
[etc/vim.git] / autoload / ale / handlers / eslint.vim
1 " Author: w0rp <devw0rp@gmail.com>
2 " Description: Functions for working with eslint, for checking or fixing files.
3
4 let s:executables = [
5 \   '.yarn/sdks/eslint/bin/eslint.js',
6 \   'node_modules/.bin/eslint_d',
7 \   'node_modules/eslint/bin/eslint.js',
8 \   'node_modules/.bin/eslint',
9 \]
10 let s:sep = has('win32') ? '\' : '/'
11
12 call ale#Set('javascript_eslint_options', '')
13 call ale#Set('javascript_eslint_executable', 'eslint')
14 call ale#Set('javascript_eslint_use_global', get(g:, 'ale_use_global_executables', 0))
15 call ale#Set('javascript_eslint_suppress_eslintignore', 0)
16 call ale#Set('javascript_eslint_suppress_missing_config', 0)
17
18 function! ale#handlers#eslint#FindConfig(buffer) abort
19     for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h'))
20         for l:basename in [
21         \   'eslint.config.js',
22         \   'eslint.config.mjs',
23         \   'eslint.config.cjs',
24         \   '.eslintrc.js',
25         \   '.eslintrc.cjs',
26         \   '.eslintrc.yaml',
27         \   '.eslintrc.yml',
28         \   '.eslintrc.json',
29         \   '.eslintrc',
30         \]
31             let l:config = ale#path#Simplify(join([l:path, l:basename], s:sep))
32
33             if filereadable(l:config)
34                 return l:config
35             endif
36         endfor
37     endfor
38
39     return ale#path#FindNearestFile(a:buffer, 'package.json')
40 endfunction
41
42 function! ale#handlers#eslint#GetExecutable(buffer) abort
43     return ale#path#FindExecutable(a:buffer, 'javascript_eslint', s:executables)
44 endfunction
45
46 " Given a buffer, return an appropriate working directory for ESLint.
47 function! ale#handlers#eslint#GetCwd(buffer) abort
48     return ale#path#Dirname(ale#handlers#eslint#FindConfig(a:buffer))
49 endfunction
50
51 function! ale#handlers#eslint#GetCommand(buffer) abort
52     let l:executable = ale#handlers#eslint#GetExecutable(a:buffer)
53
54     let l:options = ale#Var(a:buffer, 'javascript_eslint_options')
55
56     return ale#node#Executable(a:buffer, l:executable)
57     \   . (!empty(l:options) ? ' ' . l:options : '')
58     \   . ' -f json --stdin --stdin-filename %s'
59 endfunction
60
61 function! s:AddHintsForTypeScriptParsingErrors(output) abort
62     for l:item in a:output
63         let l:item.text = substitute(
64         \   l:item.text,
65         \   '^\(Parsing error\)',
66         \   '\1 (You may need configure typescript-eslint-parser)',
67         \   '',
68         \)
69     endfor
70 endfunction
71
72 function! s:CheckForBadConfig(buffer, lines) abort
73     let l:config_error_pattern = '\v^ESLint couldn''t find a configuration file'
74     \   . '|^Cannot read config file'
75     \   . '|^.*Configuration for rule .* is invalid'
76     \   . '|^ImportDeclaration should appear'
77
78     " Look for a message in the first few lines which indicates that
79     " a configuration file couldn't be found.
80     for l:line in a:lines[:10]
81         let l:match = matchlist(l:line, l:config_error_pattern)
82
83         if len(l:match) > 0
84             " Don't show the missing config error if we've disabled it.
85             if ale#Var(a:buffer, 'javascript_eslint_suppress_missing_config')
86             \&& l:match[0] is# 'ESLint couldn''t find a configuration file'
87                 return 0
88             endif
89
90             return 1
91         endif
92     endfor
93
94     return 0
95 endfunction
96
97 function! s:parseJSON(buffer, lines) abort
98     let l:parsed = []
99
100     for l:line in a:lines
101         try
102             let l:parsed = extend(l:parsed, json_decode(l:line))
103         catch
104         endtry
105     endfor
106
107     if type(l:parsed) != v:t_list || empty(l:parsed)
108         return []
109     endif
110
111     let l:errors = l:parsed[0]['messages']
112
113     if empty(l:errors)
114         return []
115     endif
116
117     let l:output = []
118
119     for l:error in l:errors
120         let l:obj = ({
121         \   'lnum': get(l:error, 'line', 0),
122         \   'text': get(l:error, 'message', ''),
123         \   'type': 'E',
124         \})
125
126         if get(l:error, 'severity', 0) is# 1
127             let l:obj.type = 'W'
128         endif
129
130         if has_key(l:error, 'ruleId')
131             let l:code = l:error['ruleId']
132
133             " Sometimes ESLint returns null here
134             if !empty(l:code)
135                 let l:obj.code = l:code
136             endif
137         endif
138
139         if has_key(l:error, 'column')
140             let l:obj.col = l:error['column']
141         endif
142
143         if has_key(l:error, 'endColumn')
144             let l:obj.end_col = l:error['endColumn'] - 1
145         endif
146
147         if has_key(l:error, 'endLine')
148             let l:obj.end_lnum = l:error['endLine']
149         endif
150
151         call add(l:output, l:obj)
152     endfor
153
154     return l:output
155 endfunction
156
157 let s:col_end_patterns = [
158 \   '\vParsing error: Unexpected token (.+) ?',
159 \   '\v''(.+)'' is not defined.',
160 \   '\v%(Unexpected|Redundant use of) [''`](.+)[''`]',
161 \   '\vUnexpected (console) statement',
162 \]
163
164 function! s:parseLines(buffer, lines) abort
165     " Matches patterns line the following:
166     "
167     " /path/to/some-filename.js:47:14: Missing trailing comma. [Warning/comma-dangle]
168     " /path/to/some-filename.js:56:41: Missing semicolon. [Error/semi]
169     let l:pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\) \[\(.\+\)\]$'
170     " This second pattern matches lines like the following:
171     "
172     " /path/to/some-filename.js:13:3: Parsing error: Unexpected token
173     let l:parsing_pattern = '^.*:\(\d\+\):\(\d\+\): \(.\+\)$'
174     let l:output = []
175
176     for l:match in ale#util#GetMatches(a:lines, [l:pattern, l:parsing_pattern])
177         let l:text = l:match[3]
178
179         let l:obj = {
180         \   'lnum': l:match[1] + 0,
181         \   'col': l:match[2] + 0,
182         \   'text': l:text,
183         \   'type': 'E',
184         \}
185
186         " Take the error type from the output if available.
187         let l:split_code = split(l:match[4], '/')
188
189         if get(l:split_code, 0, '') is# 'Warning'
190             let l:obj.type = 'W'
191         endif
192
193         " The code can be something like 'Error/foo/bar', or just 'Error'
194         if !empty(get(l:split_code, 1))
195             let l:obj.code = join(l:split_code[1:], '/')
196         endif
197
198         for l:col_match in ale#util#GetMatches(l:text, s:col_end_patterns)
199             let l:obj.end_col = l:obj.col + len(l:col_match[1]) - 1
200         endfor
201
202         call add(l:output, l:obj)
203     endfor
204
205     return l:output
206 endfunction
207
208 function! s:FilterResult(buffer, obj) abort
209     if ale#Var(a:buffer, 'javascript_eslint_suppress_eslintignore')
210         if a:obj.text =~# '^File ignored'
211             return 0
212         endif
213     endif
214
215     if has_key(a:obj, 'code') && a:obj.code is# 'no-trailing-spaces'
216     \&& !ale#Var(a:buffer, 'warn_about_trailing_whitespace')
217         return 0
218     endif
219
220     return 1
221 endfunction
222
223 function! s:HandleESLintOutput(buffer, lines, type) abort
224     if s:CheckForBadConfig(a:buffer, a:lines)
225         return [{
226         \   'lnum': 1,
227         \   'text': 'eslint configuration error (type :ALEDetail for more information)',
228         \   'detail': join(a:lines, "\n"),
229         \}]
230     endif
231
232     if a:lines == ['Could not connect']
233         return [{
234         \   'lnum': 1,
235         \   'text': 'Could not connect to eslint_d. Try updating eslint_d or killing it.',
236         \}]
237     endif
238
239     if a:type is# 'json'
240         let l:output = s:parseJSON(a:buffer, a:lines)
241     else
242         let l:output = s:parseLines(a:buffer, a:lines)
243     endif
244
245     call filter(l:output, {idx, obj -> s:FilterResult(a:buffer, obj)})
246
247     if expand('#' . a:buffer . ':t') =~? '\.tsx\?$'
248         call s:AddHintsForTypeScriptParsingErrors(l:output)
249     endif
250
251     return l:output
252 endfunction
253
254 function! ale#handlers#eslint#HandleJSON(buffer, lines) abort
255     return s:HandleESLintOutput(a:buffer, a:lines, 'json')
256 endfunction
257
258 function! ale#handlers#eslint#Handle(buffer, lines) abort
259     return s:HandleESLintOutput(a:buffer, a:lines, 'lines')
260 endfunction