From f5aa0e84577e583349b8849beae9348a0c017720 Mon Sep 17 00:00:00 2001 From: w0rp Date: Thu, 27 Aug 2020 11:44:35 +0100 Subject: [PATCH] Fix #3307 - Handle compile_commands paths better ALE now converts paths from compile_commands.json files into absolute paths and prefers matching against absolute file and directory names for determining which flags to use for files. As a result, parsing compile_commands.json to determine flags should work for a lot more C and C++ projects. --- autoload/ale/c.vim | 71 ++++++++++---- test/test_c_flag_parsing.vader | 163 ++++++++++++++++++++++++++++----- 2 files changed, 196 insertions(+), 38 deletions(-) diff --git a/autoload/ale/c.vim b/autoload/ale/c.vim index 5cf4c690..e6fcb8e6 100644 --- a/autoload/ale/c.vim +++ b/autoload/ale/c.vim @@ -84,10 +84,10 @@ function! ale#c#ExpandAtArgs(path_prefix, raw_split_lines) abort let l:path = join(split(l:option, '\zs')[1:], '') " Make path absolute - if stridx(l:path, s:sep) != 0 && stridx(l:path, '/') != 0 + if !ale#path#IsAbsolute(l:path) let l:rel_path = substitute(l:path, '"', '', 'g') let l:rel_path = substitute(l:rel_path, '''', '', 'g') - let l:path = a:path_prefix . s:sep . l:rel_path + let l:path = ale#path#GetAbsPath(a:path_prefix, l:rel_path) endif " Read the file and add all the arguments @@ -142,10 +142,12 @@ function! ale#c#ParseCFlags(path_prefix, cflag_line) abort endif " Fix relative paths if needed - if stridx(l:arg, s:sep) != 0 && stridx(l:arg, '/') != 0 + if !ale#path#IsAbsolute(l:arg) let l:rel_path = substitute(l:arg, '"', '', 'g') let l:rel_path = substitute(l:rel_path, '''', '', 'g') - let l:arg = ale#Escape(a:path_prefix . s:sep . l:rel_path) + let l:arg = ale#Escape( + \ ale#path#GetAbsPath(a:path_prefix, l:rel_path) + \) endif call add(l:cflags_list, l:option) @@ -268,6 +270,10 @@ if !exists('s:compile_commands_cache') let s:compile_commands_cache = {} endif +function! ale#c#ResetCompileCommandsCache() abort + let s:compile_commands_cache = {} +endfunction + function! s:GetLookupFromCompileCommandsFile(compile_commands_file) abort let l:empty = [{}, {}] @@ -298,9 +304,20 @@ function! s:GetLookupFromCompileCommandsFile(compile_commands_file) abort let l:dir_lookup = {} for l:entry in (type(l:raw_data) is v:t_list ? l:raw_data : []) + let l:filename = ale#path#GetAbsPath(l:entry.directory, l:entry.file) + + " Store a key for lookups by the absolute path to the filename. + let l:file_lookup[l:filename] = get(l:file_lookup, l:filename, []) + [l:entry] + + " Store a key for fuzzy lookups by the absolute path to the directory. + let l:dirname = fnamemodify(l:filename, ':h') + let l:dir_lookup[l:dirname] = get(l:dir_lookup, l:dirname, []) + [l:entry] + + " Store a key for fuzzy lookups by just the basename of the file. let l:basename = tolower(fnamemodify(l:entry.file, ':t')) let l:file_lookup[l:basename] = get(l:file_lookup, l:basename, []) + [l:entry] + " Store a key for fuzzy lookups by just the basename of the directory. let l:dirbasename = tolower(fnamemodify(l:entry.directory, ':p:h:t')) let l:dir_lookup[l:dirbasename] = get(l:dir_lookup, l:dirbasename, []) + [l:entry] endfor @@ -326,17 +343,41 @@ function! ale#c#GetCompileCommand(json_item) abort endfunction function! ale#c#ParseCompileCommandsFlags(buffer, file_lookup, dir_lookup) abort + let l:buffer_filename = ale#path#Simplify(expand('#' . a:buffer . ':p')) + let l:basename = tolower(fnamemodify(l:buffer_filename, ':t')) + " Look for any file in the same directory if we can't find an exact match. + let l:dir = fnamemodify(l:buffer_filename, ':h') + " Search for an exact file match first. - let l:basename = tolower(expand('#' . a:buffer . ':t')) - let l:file_list = get(a:file_lookup, l:basename, []) + let l:file_list = get(a:file_lookup, l:buffer_filename, []) + " Try the absolute path to the directory second. + let l:dir_list = get(a:dir_lookup, l:dir, []) + + if empty(l:file_list) && empty(l:dir_list) + " If we can't find matches with the path to the file, try a + " case-insensitive match for any similarly-named file. + let l:file_list = get(a:file_lookup, l:basename, []) + + " If we can't find matches with the path to the directory, try a + " case-insensitive match for anything in similarly-named directory. + let l:dir_list = get(a:dir_lookup, tolower(fnamemodify(l:dir, ':t')), []) + endif + " A source file matching the header filename. let l:source_file = '' if empty(l:file_list) && l:basename =~? '\.h$\|\.hpp$' for l:suffix in ['.c', '.cpp'] - let l:key = fnamemodify(l:basename, ':r') . l:suffix + " Try to find a source file by an absolute path first. + let l:key = fnamemodify(l:buffer_filename, ':r') . l:suffix let l:file_list = get(a:file_lookup, l:key, []) + if empty(l:file_list) + " Look fuzzy matches on the basename second. + let l:key = fnamemodify(l:basename, ':r') . l:suffix + let l:file_list = get(a:file_lookup, l:key, []) + endif + if !empty(l:file_list) let l:source_file = l:key break @@ -345,27 +386,25 @@ function! ale#c#ParseCompileCommandsFlags(buffer, file_lookup, dir_lookup) abort endif for l:item in l:file_list + let l:filename = ale#path#GetAbsPath(l:item.directory, l:item.file) + " Load the flags for this file, or for a source file matching the " header file. if ( - \ bufnr(l:item.file) is a:buffer + \ bufnr(l:filename) is a:buffer \ || ( \ !empty(l:source_file) - \ && l:item.file[-len(l:source_file):] is? l:source_file + \ && l:filename[-len(l:source_file):] is? l:source_file \ ) \) return ale#c#ParseCFlags(l:item.directory, ale#c#GetCompileCommand(l:item)) endif endfor - " Look for any file in the same directory if we can't find an exact match. - let l:dir = ale#path#Simplify(expand('#' . a:buffer . ':p:h')) - - let l:dirbasename = tolower(expand('#' . a:buffer . ':p:h:t')) - let l:dir_list = get(a:dir_lookup, l:dirbasename, []) - for l:item in l:dir_list - if ale#path#Simplify(fnamemodify(l:item.file, ':h')) is? l:dir + let l:filename = ale#path#GetAbsPath(l:item.directory, l:item.file) + + if ale#path#Simplify(fnamemodify(l:filename, ':h')) is? l:dir return ale#c#ParseCFlags(l:item.directory, ale#c#GetCompileCommand(l:item)) endif endfor diff --git a/test/test_c_flag_parsing.vader b/test/test_c_flag_parsing.vader index e9830db8..e33a29ea 100644 --- a/test/test_c_flag_parsing.vader +++ b/test/test_c_flag_parsing.vader @@ -178,44 +178,164 @@ Execute(ParseCompileCommandsFlags should tolerate empty values): Execute(ParseCompileCommandsFlags should parse some basic flags): silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c')) + " We should read the absolute path filename entry, not the other ones. AssertEqual \ '-I ' . ale#path#Simplify('/usr/include/xmms2'), - \ ale#c#ParseCompileCommandsFlags(bufnr(''), { "xmms2-mpris.c": [ + \ ale#c#ParseCompileCommandsFlags( + \ bufnr(''), \ { - \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), - \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') - \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' - \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), - \ 'file': ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'): [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ }, + \ ], + \ "xmms2-mpris.c": [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/ignoreme') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ }, + \ ], \ }, - \ ] }, {}) + \ { + \ ale#path#Simplify('/foo/bar/xmms2-mpris/src'): [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris/src'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/ignoreme') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': 'other.c', + \ }, + \ ], + \ "src": [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/ignoreme') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify((has('win32') ? 'C:' : '') . '/foo/bar/xmms2-mpris/src/xmms2-other.c'), + \ }, + \ ], + \ }, + \ ) -Execute(ParseCompileCommandsFlags should tolerate items without commands): +Execute(ParseCompileCommandsFlags should fall back to files with the same name): silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c')) + " We should prefer the basename file flags, not the base dirname flags. AssertEqual - \ '', - \ ale#c#ParseCompileCommandsFlags(bufnr(''), { "xmms2-mpris.c": [ + \ '-I ' . ale#path#Simplify('/usr/include/xmms2'), + \ ale#c#ParseCompileCommandsFlags( + \ bufnr(''), \ { - \ 'directory': '/foo/bar/xmms2-mpris', - \ 'file': '/foo/bar/xmms2-mpris/src/xmms2-mpris.c', + \ "xmms2-mpris.c": [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ }, + \ ], \ }, - \ ] }, {}) + \ { + \ "src": [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/ignoreme') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify((has('win32') ? 'C:' : '') . '/foo/bar/xmms2-mpris/src/xmms2-other.c'), + \ }, + \ ], + \ }, + \ ) + +Execute(ParseCompileCommandsFlags should parse flags for exact directory matches): + silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c')) + + " We should ues the exact directory flags, not the file basename flags. + AssertEqual + \ '-I ' . ale#path#Simplify('/usr/include/xmms2'), + \ ale#c#ParseCompileCommandsFlags( + \ bufnr(''), + \ { + \ "xmms2-mpris.c": [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/ignoreme') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ }, + \ ], + \ }, + \ { + \ ale#path#Simplify('/foo/bar/xmms2-mpris/src'): [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris/src'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': 'other.c', + \ }, + \ ], + \ "src": [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/ignoreme') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify((has('win32') ? 'C:' : '') . '/foo/bar/xmms2-mpris/src/xmms2-other.c'), + \ }, + \ ], + \ }, + \ ) Execute(ParseCompileCommandsFlags should fall back to files in the same directory): silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c')) AssertEqual \ '-I ' . ale#path#Simplify('/usr/include/xmms2'), - \ ale#c#ParseCompileCommandsFlags(bufnr(''), {}, { "src": [ + \ ale#c#ParseCompileCommandsFlags( + \ bufnr(''), + \ {}, \ { - \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), - \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') - \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' - \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), - \ 'file': ale#path#Simplify((has('win32') ? 'C:' : '') . '/foo/bar/xmms2-mpris/src/xmms2-other.c'), + \ "src": [ + \ { + \ 'directory': ale#path#Simplify('/foo/bar/xmms2-mpris'), + \ 'command': '/usr/bin/cc -I' . ale#path#Simplify('/usr/include/xmms2') + \ . ' -o CMakeFiles/xmms2-mpris.dir/src/xmms2-mpris.c.o' + \ . ' -c ' . ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c'), + \ 'file': ale#path#Simplify((has('win32') ? 'C:' : '') . '/foo/bar/xmms2-mpris/src/xmms2-other.c'), + \ }, + \ ], \ }, - \ ] }) + \ ) + +Execute(ParseCompileCommandsFlags should tolerate items without commands): + silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.c')) + + AssertEqual + \ '', + \ ale#c#ParseCompileCommandsFlags( + \ bufnr(''), + \ { + \ "xmms2-mpris.c": [ + \ { + \ 'directory': '/foo/bar/xmms2-mpris', + \ 'file': '/foo/bar/xmms2-mpris/src/xmms2-mpris.c', + \ }, + \ ], + \ }, + \ {}, + \ ) Execute(ParseCompileCommandsFlags should take commands from matching .c files for .h files): silent noautocmd execute 'file! ' . fnameescape(ale#path#Simplify('/foo/bar/xmms2-mpris/src/xmms2-mpris.h')) @@ -235,8 +355,7 @@ Execute(ParseCompileCommandsFlags should take commands from matching .c files fo \ }, \ ], \ }, - \ { - \ }, + \ {}, \ ) Execute(ParseCompileCommandsFlags should take commands from matching .cpp files for .hpp files):