]> git.madduck.net Git - etc/vim.git/blob - autoload/ale/c.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 / c.vim
1 " Author: gagbo <gagbobada@gmail.com>, w0rp <devw0rp@gmail.com>, roel0 <postelmansroel@gmail.com>
2 " Description: Functions for integrating with C-family linters.
3
4 call ale#Set('c_parse_makefile', 0)
5 call ale#Set('c_always_make', has('unix') && !has('macunix'))
6 call ale#Set('c_parse_compile_commands', 1)
7
8 let s:sep = has('win32') ? '\' : '/'
9
10 " Set just so tests can override it.
11 let g:__ale_c_project_filenames = ['.git/HEAD', 'configure', 'Makefile', 'CMakeLists.txt']
12
13 let g:ale_c_build_dir_names = get(g:, 'ale_c_build_dir_names', [
14 \   'build',
15 \   'bin',
16 \])
17
18 function! s:CanParseMakefile(buffer) abort
19     " Something somewhere seems to delete this setting in tests, so ensure we
20     " always have a default value.
21     call ale#Set('c_parse_makefile', 0)
22
23     return ale#Var(a:buffer, 'c_parse_makefile')
24 endfunction
25
26 function! ale#c#GetBuildDirectory(buffer) abort
27     let l:build_dir = ale#Var(a:buffer, 'c_build_dir')
28
29     " c_build_dir has the priority if defined
30     if !empty(l:build_dir)
31         return l:build_dir
32     endif
33
34     let [l:root, l:json_file] = ale#c#FindCompileCommands(a:buffer)
35
36     return ale#path#Dirname(l:json_file)
37 endfunction
38
39 function! ale#c#ShellSplit(line) abort
40     let l:stack = []
41     let l:args = ['']
42     let l:prev = ''
43
44     for l:char in split(a:line, '\zs')
45         if l:char is# ''''
46             if len(l:stack) > 0 && get(l:stack, -1) is# ''''
47                 call remove(l:stack, -1)
48             elseif (len(l:stack) == 0 || get(l:stack, -1) isnot# '"') && l:prev isnot# '\'
49                 call add(l:stack, l:char)
50             endif
51         elseif (l:char is# '"' || l:char is# '`') && l:prev isnot# '\'
52             if len(l:stack) > 0 && get(l:stack, -1) is# l:char
53                 call remove(l:stack, -1)
54             elseif len(l:stack) == 0 || get(l:stack, -1) isnot# ''''
55                 call add(l:stack, l:char)
56             endif
57         elseif (l:char is# '(' || l:char is# '[' || l:char is# '{') && l:prev isnot# '\'
58             if len(l:stack) == 0 || get(l:stack, -1) isnot# ''''
59                 call add(l:stack, l:char)
60             endif
61         elseif (l:char is# ')' || l:char is# ']' || l:char is# '}') && l:prev isnot# '\'
62             if len(l:stack) > 0 && get(l:stack, -1) is# {')': '(', ']': '[', '}': '{'}[l:char]
63                 call remove(l:stack, -1)
64             endif
65         elseif l:char is# ' ' && len(l:stack) == 0
66             if len(get(l:args, -1)) > 0
67                 call add(l:args, '')
68             endif
69
70             continue
71         endif
72
73         let l:args[-1] = get(l:args, -1) . l:char
74     endfor
75
76     return l:args
77 endfunction
78
79 " Takes the path prefix and a list of cflags and expands @file arguments to
80 " the contents of the file.
81 "
82 " @file arguments are command line arguments recognised by gcc and clang. For
83 " instance, if @./path/to/file was given to gcc, it would load .path/to/file
84 " and use the contents of that file as arguments.
85 function! ale#c#ExpandAtArgs(path_prefix, raw_split_lines) abort
86     let l:out_lines = []
87
88     for l:option in a:raw_split_lines
89         if stridx(l:option, '@') == 0
90             " This is an argument specifying a location of a file containing other arguments
91             let l:path = join(split(l:option, '\zs')[1:], '')
92
93             " Make path absolute
94             if !ale#path#IsAbsolute(l:path)
95                 let l:rel_path = substitute(l:path, '"', '', 'g')
96                 let l:rel_path = substitute(l:rel_path, '''', '', 'g')
97                 let l:path = ale#path#GetAbsPath(a:path_prefix, l:rel_path)
98             endif
99
100             " Read the file and add all the arguments
101             try
102                 let l:additional_args = readfile(l:path)
103             catch
104                 continue " All we can really do is skip this argument
105             endtry
106
107             let l:file_lines = []
108
109             for l:line in l:additional_args
110                 let l:file_lines += ale#c#ShellSplit(l:line)
111             endfor
112
113             " @file arguments can include other @file arguments, so we must
114             " recurse.
115             let l:out_lines += ale#c#ExpandAtArgs(a:path_prefix, l:file_lines)
116         else
117             " This is not an @file argument, so don't touch it.
118             let l:out_lines += [l:option]
119         endif
120     endfor
121
122     return l:out_lines
123 endfunction
124
125 " Quote C/C++ a compiler argument, if needed.
126 "
127 " Quoting arguments might cause issues with some systems/compilers, so we only
128 " quote them if we need to.
129 function! ale#c#QuoteArg(arg) abort
130     if a:arg !~# '\v[#$&*()\\|[\]{};''"<>/?! ^%]'
131         return a:arg
132     endif
133
134     return ale#Escape(a:arg)
135 endfunction
136
137 function! ale#c#ParseCFlags(path_prefix, should_quote, raw_arguments) abort
138     " Expand @file arguments now before parsing
139     let l:arguments = ale#c#ExpandAtArgs(a:path_prefix, a:raw_arguments)
140     " A list of [already_quoted, argument]
141     let l:items = []
142     let l:option_index = 0
143
144     while l:option_index < len(l:arguments)
145         let l:option = l:arguments[l:option_index]
146         let l:option_index = l:option_index + 1
147
148         " Include options, that may need relative path fix
149         if stridx(l:option, '-I') == 0
150         \ || stridx(l:option, '-iquote') == 0
151         \ || stridx(l:option, '-isystem') == 0
152         \ || stridx(l:option, '-idirafter') == 0
153         \ || stridx(l:option, '-iframework') == 0
154             if stridx(l:option, '-I') == 0 && l:option isnot# '-I'
155                 let l:arg = join(split(l:option, '\zs')[2:], '')
156                 let l:option = '-I'
157             else
158                 let l:arg = l:arguments[l:option_index]
159                 let l:option_index = l:option_index + 1
160             endif
161
162             " Fix relative paths if needed
163             if !ale#path#IsAbsolute(l:arg)
164                 let l:rel_path = substitute(l:arg, '"', '', 'g')
165                 let l:rel_path = substitute(l:rel_path, '''', '', 'g')
166                 let l:arg = ale#path#GetAbsPath(a:path_prefix, l:rel_path)
167             endif
168
169             call add(l:items, [1, l:option])
170             call add(l:items, [1, ale#Escape(l:arg)])
171         " Options with arg that can be grouped with the option or separate
172         elseif stridx(l:option, '-D') == 0 || stridx(l:option, '-B') == 0
173             if l:option is# '-D' || l:option is# '-B'
174                 call add(l:items, [1, l:option])
175                 call add(l:items, [0, l:arguments[l:option_index]])
176                 let l:option_index = l:option_index + 1
177             else
178                 call add(l:items, [0, l:option])
179             endif
180         " Options that have an argument (always separate)
181         elseif l:option is# '-iprefix' || stridx(l:option, '-iwithprefix') == 0
182         \ || l:option is# '-isysroot' || l:option is# '-imultilib'
183         \ || l:option is# '-include' || l:option is# '-imacros'
184             call add(l:items, [0, l:option])
185             call add(l:items, [0, l:arguments[l:option_index]])
186             let l:option_index = l:option_index + 1
187         " Options without argument
188         elseif (stridx(l:option, '-W') == 0 && stridx(l:option, '-Wa,') != 0 && stridx(l:option, '-Wl,') != 0 && stridx(l:option, '-Wp,') != 0)
189         \ || l:option is# '-w' || stridx(l:option, '-pedantic') == 0
190         \ || l:option is# '-ansi' || stridx(l:option, '-std=') == 0
191         \ || stridx(l:option, '-f') == 0 && l:option !~# '\v^-f(dump|diagnostics|no-show-column|stack-usage)'
192         \ || stridx(l:option, '-O') == 0
193         \ || l:option is# '-C' || l:option is# '-CC' || l:option is# '-trigraphs'
194         \ || stridx(l:option, '-nostdinc') == 0 || stridx(l:option, '-iplugindir=') == 0
195         \ || stridx(l:option, '--sysroot=') == 0 || l:option is# '--no-sysroot-suffix'
196         \ || stridx(l:option, '-m') == 0
197             call add(l:items, [0, l:option])
198         endif
199     endwhile
200
201     if a:should_quote
202         " Quote C arguments that haven't already been quoted above.
203         " If and only if we've been asked to quote them.
204         call map(l:items, 'v:val[0] ? v:val[1] : ale#c#QuoteArg(v:val[1])')
205     else
206         call map(l:items, 'v:val[1]')
207     endif
208
209     return join(l:items, ' ')
210 endfunction
211
212 function! ale#c#ParseCFlagsFromMakeOutput(buffer, make_output) abort
213     if !s:CanParseMakefile(a:buffer)
214         return v:null
215     endif
216
217     let l:buffer_filename = expand('#' . a:buffer . ':t')
218     let l:cflag_line = ''
219
220     " Find a line matching this buffer's filename in the make output.
221     for l:line in a:make_output
222         if stridx(l:line, l:buffer_filename) >= 0
223             let l:cflag_line = l:line
224             break
225         endif
226     endfor
227
228     let l:makefile_path = ale#path#FindNearestFile(a:buffer, 'Makefile')
229     let l:makefile_dir = fnamemodify(l:makefile_path, ':p:h')
230
231     return ale#c#ParseCFlags(l:makefile_dir, 0, ale#c#ShellSplit(l:cflag_line))
232 endfunction
233
234 " Given a buffer number, find the project directory containing
235 " compile_commands.json, and the path to the compile_commands.json file.
236 "
237 " If compile_commands.json cannot be found, two empty strings will be
238 " returned.
239 function! ale#c#FindCompileCommands(buffer) abort
240     " Look above the current source file to find compile_commands.json
241     let l:json_file = ale#path#FindNearestFile(a:buffer, 'compile_commands.json')
242
243     if !empty(l:json_file)
244         return [fnamemodify(l:json_file, ':h'), l:json_file]
245     endif
246
247     " Search in build directories if we can't find it in the project.
248     for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h'))
249         for l:dirname in ale#Var(a:buffer, 'c_build_dir_names')
250             let l:c_build_dir = l:path . s:sep . l:dirname
251             let l:json_file = l:c_build_dir . s:sep . 'compile_commands.json'
252
253             if filereadable(l:json_file)
254                 return [l:path, l:json_file]
255             endif
256         endfor
257     endfor
258
259     return ['', '']
260 endfunction
261
262 " Find the project root for C/C++ projects.
263 "
264 " The location of compile_commands.json will be used to find project roots.
265 "
266 " If compile_commands.json cannot be found, other common configuration files
267 " will be used to detect the project root.
268 function! ale#c#FindProjectRoot(buffer) abort
269     let [l:root, l:json_file] = ale#c#FindCompileCommands(a:buffer)
270
271     " Fall back on detecting the project root based on other filenames.
272     if empty(l:root)
273         for l:project_filename in g:__ale_c_project_filenames
274             let l:full_path = ale#path#FindNearestFile(a:buffer, l:project_filename)
275
276             if !empty(l:full_path)
277                 let l:path = fnamemodify(l:full_path, ':h')
278
279                 " Correct .git path detection.
280                 if fnamemodify(l:path, ':t') is# '.git'
281                     let l:path = fnamemodify(l:path, ':h')
282                 endif
283
284                 return l:path
285             endif
286         endfor
287     endif
288
289     return l:root
290 endfunction
291
292 " Cache compile_commands.json data in a Dictionary, so we don't need to read
293 " the same files over and over again. The key in the dictionary will include
294 " the last modified time of the file.
295 if !exists('s:compile_commands_cache')
296     let s:compile_commands_cache = {}
297 endif
298
299 function! ale#c#ResetCompileCommandsCache() abort
300     let s:compile_commands_cache = {}
301 endfunction
302
303 function! s:GetLookupFromCompileCommandsFile(compile_commands_file) abort
304     let l:empty = [{}, {}]
305
306     if empty(a:compile_commands_file)
307         return l:empty
308     endif
309
310     let l:time = getftime(a:compile_commands_file)
311
312     if l:time < 0
313         return l:empty
314     endif
315
316     let l:key = a:compile_commands_file . ':' . l:time
317
318     if has_key(s:compile_commands_cache, l:key)
319         return s:compile_commands_cache[l:key]
320     endif
321
322     let l:raw_data = []
323     silent! let l:raw_data = json_decode(join(readfile(a:compile_commands_file), ''))
324
325     if type(l:raw_data) isnot v:t_list
326         let l:raw_data = []
327     endif
328
329     let l:file_lookup = {}
330     let l:dir_lookup = {}
331
332     for l:entry in (type(l:raw_data) is v:t_list ? l:raw_data : [])
333         let l:filename = ale#path#GetAbsPath(l:entry.directory, l:entry.file)
334
335         " Store a key for lookups by the absolute path to the filename.
336         let l:file_lookup[l:filename] = get(l:file_lookup, l:filename, []) + [l:entry]
337
338         " Store a key for fuzzy lookups by the absolute path to the directory.
339         let l:dirname = fnamemodify(l:filename, ':h')
340         let l:dir_lookup[l:dirname] = get(l:dir_lookup, l:dirname, []) + [l:entry]
341
342         " Store a key for fuzzy lookups by just the basename of the file.
343         let l:basename = tolower(fnamemodify(l:entry.file, ':t'))
344         let l:file_lookup[l:basename] = get(l:file_lookup, l:basename, []) + [l:entry]
345
346         " Store a key for fuzzy lookups by just the basename of the directory.
347         let l:dirbasename = tolower(fnamemodify(l:entry.directory, ':p:h:t'))
348         let l:dir_lookup[l:dirbasename] = get(l:dir_lookup, l:dirbasename, []) + [l:entry]
349     endfor
350
351     if !empty(l:file_lookup) && !empty(l:dir_lookup)
352         let l:result = [l:file_lookup, l:dir_lookup]
353         let s:compile_commands_cache[l:key] = l:result
354
355         return l:result
356     endif
357
358     return l:empty
359 endfunction
360
361 " Get [should_quote, arguments] from either 'command' or 'arguments'
362 " 'arguments' should be quoted later, the split 'command' strings should not.
363 function! s:GetArguments(json_item) abort
364     if has_key(a:json_item, 'arguments')
365         return [1, a:json_item.arguments]
366     elseif has_key(a:json_item, 'command')
367         return [0, ale#c#ShellSplit(a:json_item.command)]
368     endif
369
370     return [0, []]
371 endfunction
372
373 function! ale#c#ParseCompileCommandsFlags(buffer, file_lookup, dir_lookup) abort
374     let l:buffer_filename = ale#path#Simplify(expand('#' . a:buffer . ':p'))
375     let l:basename = tolower(fnamemodify(l:buffer_filename, ':t'))
376     " Look for any file in the same directory if we can't find an exact match.
377     let l:dir = fnamemodify(l:buffer_filename, ':h')
378
379     " Search for an exact file match first.
380     let l:file_list = get(a:file_lookup, l:buffer_filename, [])
381
382     " We may have to look for /foo/bar instead of C:\foo\bar
383     if empty(l:file_list) && has('win32')
384         let l:file_list = get(
385         \   a:file_lookup,
386         \   ale#path#RemoveDriveLetter(l:buffer_filename),
387         \   []
388         \)
389     endif
390
391     " Try the absolute path to the directory second.
392     let l:dir_list = get(a:dir_lookup, l:dir, [])
393
394     if empty(l:dir_list) && has('win32')
395         let l:dir_list = get(
396         \   a:dir_lookup,
397         \   ale#path#RemoveDriveLetter(l:dir),
398         \   []
399         \)
400     endif
401
402     if empty(l:file_list) && empty(l:dir_list)
403         " If we can't find matches with the path to the file, try a
404         " case-insensitive match for any similarly-named file.
405         let l:file_list = get(a:file_lookup, l:basename, [])
406
407         " If we can't find matches with the path to the directory, try a
408         " case-insensitive match for anything in similarly-named directory.
409         let l:dir_list = get(a:dir_lookup, tolower(fnamemodify(l:dir, ':t')), [])
410     endif
411
412     " A source file matching the header filename.
413     let l:source_file = ''
414
415     if empty(l:file_list) && l:basename =~? '\.h$\|\.hpp$'
416         for l:suffix in ['.c', '.cpp']
417             " Try to find a source file by an absolute path first.
418             let l:key = fnamemodify(l:buffer_filename, ':r') . l:suffix
419             let l:file_list = get(a:file_lookup, l:key, [])
420
421             if empty(l:file_list) && has('win32')
422                 let l:file_list = get(
423                 \   a:file_lookup,
424                 \   ale#path#RemoveDriveLetter(l:key),
425                 \   []
426                 \)
427             endif
428
429             if empty(l:file_list)
430                 " Look fuzzy matches on the basename second.
431                 let l:key = fnamemodify(l:basename, ':r') . l:suffix
432                 let l:file_list = get(a:file_lookup, l:key, [])
433             endif
434
435             if !empty(l:file_list)
436                 let l:source_file = l:key
437                 break
438             endif
439         endfor
440     endif
441
442     for l:item in l:file_list
443         let l:filename = ale#path#GetAbsPath(l:item.directory, l:item.file)
444
445         " Load the flags for this file, or for a source file matching the
446         " header file.
447         if (
448         \   bufnr(l:filename) is a:buffer
449         \   || (
450         \       !empty(l:source_file)
451         \       && l:filename[-len(l:source_file):] is? l:source_file
452         \   )
453         \)
454             let [l:should_quote, l:args] = s:GetArguments(l:item)
455
456             return ale#c#ParseCFlags(l:item.directory, l:should_quote, l:args)
457         endif
458     endfor
459
460     for l:item in l:dir_list
461         let l:filename = ale#path#GetAbsPath(l:item.directory, l:item.file)
462
463         if ale#path#RemoveDriveLetter(fnamemodify(l:filename, ':h'))
464         \  is? ale#path#RemoveDriveLetter(l:dir)
465             let [l:should_quote, l:args] = s:GetArguments(l:item)
466
467             return ale#c#ParseCFlags(l:item.directory, l:should_quote, l:args)
468         endif
469     endfor
470
471     return ''
472 endfunction
473
474 function! ale#c#FlagsFromCompileCommands(buffer, compile_commands_file) abort
475     let l:lookups = s:GetLookupFromCompileCommandsFile(a:compile_commands_file)
476     let l:file_lookup = l:lookups[0]
477     let l:dir_lookup = l:lookups[1]
478
479     return ale#c#ParseCompileCommandsFlags(a:buffer, l:file_lookup, l:dir_lookup)
480 endfunction
481
482 function! ale#c#GetCFlags(buffer, output) abort
483     let l:cflags = v:null
484
485     if ale#Var(a:buffer, 'c_parse_compile_commands')
486         let [l:root, l:json_file] = ale#c#FindCompileCommands(a:buffer)
487
488         if !empty(l:json_file)
489             let l:cflags = ale#c#FlagsFromCompileCommands(a:buffer, l:json_file)
490         endif
491     endif
492
493     if empty(l:cflags) && s:CanParseMakefile(a:buffer) && !empty(a:output)
494         let l:cflags = ale#c#ParseCFlagsFromMakeOutput(a:buffer, a:output)
495     endif
496
497     if l:cflags is v:null
498         let l:cflags = ale#c#IncludeOptions(ale#c#FindLocalHeaderPaths(a:buffer))
499     endif
500
501     return l:cflags isnot v:null ? l:cflags : ''
502 endfunction
503
504 function! ale#c#GetMakeCommand(buffer) abort
505     if s:CanParseMakefile(a:buffer)
506         let l:path = ale#path#FindNearestFile(a:buffer, 'Makefile')
507
508         if empty(l:path)
509             let l:path = ale#path#FindNearestFile(a:buffer, 'GNUmakefile')
510         endif
511
512         if !empty(l:path)
513             let l:always_make = ale#Var(a:buffer, 'c_always_make')
514
515             return [
516             \   fnamemodify(l:path, ':h'),
517             \   'make -n' . (l:always_make ? ' --always-make' : ''),
518             \]
519         endif
520     endif
521
522     return ['', '']
523 endfunction
524
525 function! ale#c#RunMakeCommand(buffer, Callback) abort
526     let [l:cwd, l:command] = ale#c#GetMakeCommand(a:buffer)
527
528     if empty(l:command)
529         return a:Callback(a:buffer, [])
530     endif
531
532     return ale#command#Run(
533     \   a:buffer,
534     \   l:command,
535     \   {b, output -> a:Callback(a:buffer, output)},
536     \   {'cwd': l:cwd},
537     \)
538 endfunction
539
540 " Given a buffer number, search for a project root, and output a List
541 " of directories to include based on some heuristics.
542 "
543 " For projects with headers in the project root, the project root will
544 " be returned.
545 "
546 " For projects with an 'include' directory, that directory will be returned.
547 function! ale#c#FindLocalHeaderPaths(buffer) abort
548     let l:project_root = ale#c#FindProjectRoot(a:buffer)
549
550     if empty(l:project_root)
551         return []
552     endif
553
554     " See if we can find .h files directory in the project root.
555     " If we can, that's our include directory.
556     if !empty(globpath(l:project_root, '*.h', 0))
557         return [l:project_root]
558     endif
559
560     " Look for .hpp files too.
561     if !empty(globpath(l:project_root, '*.hpp', 0))
562         return [l:project_root]
563     endif
564
565     " If we find an 'include' directory in the project root, then use that.
566     if isdirectory(l:project_root . '/include')
567         return [ale#path#Simplify(l:project_root . s:sep . 'include')]
568     endif
569
570     return []
571 endfunction
572
573 " Given a List of include paths, create a string containing the -I include
574 " options for those paths, with the paths escaped for use in the shell.
575 function! ale#c#IncludeOptions(include_paths) abort
576     let l:option_list = []
577
578     for l:path in a:include_paths
579         call add(l:option_list, '-I' . ale#Escape(l:path))
580     endfor
581
582     if empty(l:option_list)
583         return ''
584     endif
585
586     return join(l:option_list)
587 endfunction
588
589 " Get the language flag depending on on the executable, options and
590 " file extension
591 function! ale#c#GetLanguageFlag(
592 \   buffer,
593 \   executable,
594 \   use_header_lang_flag,
595 \   header_exts,
596 \   linter_lang_flag
597 \) abort
598     " Use only '-header' if the executable is 'clang' by default
599     if a:use_header_lang_flag == -1
600         let l:use_header_lang_flag = a:executable =~# 'clang'
601     else
602         let l:use_header_lang_flag = a:use_header_lang_flag
603     endif
604
605     " If we don't use the header language flag, return the default linter
606     " language flag
607     if !l:use_header_lang_flag
608         return a:linter_lang_flag
609     endif
610
611     " Get the buffer file extension
612     let l:buf_ext = expand('#' . a:buffer . ':e')
613
614     " If the buffer file is an header according to its extension, use
615     " the linter language flag + '-header', ex: 'c-header'
616     if index(a:header_exts, l:buf_ext) >= 0
617         return a:linter_lang_flag . '-header'
618     endif
619
620     " Else, use the default linter language flag
621     return a:linter_lang_flag
622 endfunction