Fix #1210 - Fix a Windows path issue which broke TSLint

This commit is contained in:
w0rp 2017-12-18 13:27:59 +00:00
parent 31241e9ed8
commit fdaac9bd78
7 changed files with 106 additions and 83 deletions

View file

@ -1,6 +1,8 @@
" Author: gagbo <gagbobada@gmail.com>, w0rp <devw0rp@gmail.com> " Author: gagbo <gagbobada@gmail.com>, w0rp <devw0rp@gmail.com>
" Description: Functions for integrating with C-family linters. " Description: Functions for integrating with C-family linters.
let s:sep = has('win32') ? '\' : '/'
function! ale#c#FindProjectRoot(buffer) abort function! ale#c#FindProjectRoot(buffer) abort
for l:project_filename in ['.git/HEAD', 'configure', 'Makefile', 'CMakeLists.txt'] for l:project_filename in ['.git/HEAD', 'configure', 'Makefile', 'CMakeLists.txt']
let l:full_path = ale#path#FindNearestFile(a:buffer, l:project_filename) let l:full_path = ale#path#FindNearestFile(a:buffer, l:project_filename)
@ -47,7 +49,7 @@ function! ale#c#FindLocalHeaderPaths(buffer) abort
" If we find an 'include' directory in the project root, then use that. " If we find an 'include' directory in the project root, then use that.
if isdirectory(l:project_root . '/include') if isdirectory(l:project_root . '/include')
return [ale#path#Simplify(l:project_root . '/include')] return [ale#path#Simplify(l:project_root . s:sep . 'include')]
endif endif
return [] return []
@ -79,7 +81,7 @@ let g:ale_c_build_dir_names = get(g:, 'ale_c_build_dir_names', [
function! ale#c#FindCompileCommands(buffer) abort function! ale#c#FindCompileCommands(buffer) abort
for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h')) for l:path in ale#path#Upwards(expand('#' . a:buffer . ':p:h'))
for l:dirname in ale#Var(a:buffer, 'c_build_dir_names') for l:dirname in ale#Var(a:buffer, 'c_build_dir_names')
let l:c_build_dir = l:path . '/' . l:dirname let l:c_build_dir = l:path . s:sep . l:dirname
if filereadable(l:c_build_dir . '/compile_commands.json') if filereadable(l:c_build_dir . '/compile_commands.json')
return l:c_build_dir return l:c_build_dir

View file

@ -1,14 +1,23 @@
" Author: w0rp <devw0rp@gmail.com> " Author: w0rp <devw0rp@gmail.com>
" Description: Functions for working with paths in the filesystem. " Description: Functions for working with paths in the filesystem.
" simplify a path, and fix annoying issues with paths on Windows.
"
" Forward slashes are changed to back slashes so path equality works better.
"
" Paths starting with more than one forward slash are changed to only one
" forward slash, to prevent the paths being treated as special MSYS paths.
function! ale#path#Simplify(path) abort function! ale#path#Simplify(path) abort
" //foo is turned into /foo to stop Windows doing stupid things with if has('unix')
" search paths. return substitute(simplify(a:path), '^//\+', '/', 'g') " no-custom-checks
return substitute(simplify(a:path), '^//\+', '/', 'g') " no-custom-checks endif
let l:win_path = substitute(a:path, '/', '\\', 'g')
return substitute(simplify(l:win_path), '^\\\+', '\', 'g') " no-custom-checks
endfunction endfunction
" This function is mainly used for testing. " This function is mainly used for testing.
" Simplify() a path, and change forward slashes to back slashes on Windows.
" "
" If an additional 'add_drive' argument is given, the current drive letter " If an additional 'add_drive' argument is given, the current drive letter
" will be prefixed to any absolute paths on Windows. " will be prefixed to any absolute paths on Windows.
@ -16,8 +25,6 @@ function! ale#path#Winify(path, ...) abort
let l:new_path = ale#path#Simplify(a:path) let l:new_path = ale#path#Simplify(a:path)
if has('win32') if has('win32')
let l:new_path = substitute(l:new_path, '/', '\\', 'g')
" Add a drive letter to \foo\bar paths, if needed. " Add a drive letter to \foo\bar paths, if needed.
if a:0 && a:1 is# 'add_drive' && l:new_path[:0] is# '\' if a:0 && a:1 is# 'add_drive' && l:new_path[:0] is# '\'
let l:new_path = fnamemodify('.', ':p')[:1] . l:new_path let l:new_path = fnamemodify('.', ':p')[:1] . l:new_path
@ -86,6 +93,10 @@ endfunction
" Return 1 if a path is an absolute path. " Return 1 if a path is an absolute path.
function! ale#path#IsAbsolute(filename) abort function! ale#path#IsAbsolute(filename) abort
if has('win32') && a:filename[:0] is# '\'
return 1
endif
" Check for /foo and C:\foo, etc. " Check for /foo and C:\foo, etc.
return a:filename[:0] is# '/' || a:filename[1:2] is# ':\' return a:filename[:0] is# '/' || a:filename[1:2] is# ':\'
endfunction endfunction
@ -103,7 +114,7 @@ endfunction
" directory, return the absolute path to the file. " directory, return the absolute path to the file.
function! ale#path#GetAbsPath(base_directory, filename) abort function! ale#path#GetAbsPath(base_directory, filename) abort
if ale#path#IsAbsolute(a:filename) if ale#path#IsAbsolute(a:filename)
return a:filename return ale#path#Simplify(a:filename)
endif endif
let l:sep = has('win32') ? '\' : '/' let l:sep = has('win32') ? '\' : '/'
@ -145,8 +156,8 @@ endfunction
" Given a path, return every component of the path, moving upwards. " Given a path, return every component of the path, moving upwards.
function! ale#path#Upwards(path) abort function! ale#path#Upwards(path) abort
let l:pattern = ale#Has('win32') ? '\v/+|\\+' : '\v/+' let l:pattern = has('win32') ? '\v/+|\\+' : '\v/+'
let l:sep = ale#Has('win32') ? '\' : '/' let l:sep = has('win32') ? '\' : '/'
let l:parts = split(ale#path#Simplify(a:path), l:pattern) let l:parts = split(ale#path#Simplify(a:path), l:pattern)
let l:path_list = [] let l:path_list = []
@ -155,7 +166,7 @@ function! ale#path#Upwards(path) abort
let l:parts = l:parts[:-2] let l:parts = l:parts[:-2]
endwhile endwhile
if ale#Has('win32') && a:path =~# '^[a-zA-z]:\' if has('win32') && a:path =~# '^[a-zA-z]:\'
" Add \ to C: for C:\, etc. " Add \ to C: for C:\, etc.
let l:path_list[-1] .= '\' let l:path_list[-1] .= '\'
elseif a:path[0] is# '/' elseif a:path[0] is# '/'

View file

@ -12,33 +12,33 @@ Execute(The javac handler should handle cannot find symbol errors):
AssertEqual AssertEqual
\ [ \ [
\ { \ {
\ 'filename': '/tmp/vLPr4Q5/33/foo.java', \ 'filename': ale#path#Simplify('/tmp/vLPr4Q5/33/foo.java'),
\ 'lnum': 1, \ 'lnum': 1,
\ 'text': 'error: some error', \ 'text': 'error: some error',
\ 'type': 'E', \ 'type': 'E',
\ }, \ },
\ { \ {
\ 'filename': '/tmp/vLPr4Q5/33/foo.java', \ 'filename': ale#path#Simplify('/tmp/vLPr4Q5/33/foo.java'),
\ 'lnum': 2, \ 'lnum': 2,
\ 'col': 5, \ 'col': 5,
\ 'text': 'error: cannot find symbol: BadName', \ 'text': 'error: cannot find symbol: BadName',
\ 'type': 'E', \ 'type': 'E',
\ }, \ },
\ { \ {
\ 'filename': '/tmp/vLPr4Q5/33/foo.java', \ 'filename': ale#path#Simplify('/tmp/vLPr4Q5/33/foo.java'),
\ 'lnum': 34, \ 'lnum': 34,
\ 'col': 5, \ 'col': 5,
\ 'text': 'error: cannot find symbol: BadName2', \ 'text': 'error: cannot find symbol: BadName2',
\ 'type': 'E', \ 'type': 'E',
\ }, \ },
\ { \ {
\ 'filename': '/tmp/vLPr4Q5/33/foo.java', \ 'filename': ale#path#Simplify('/tmp/vLPr4Q5/33/foo.java'),
\ 'lnum': 37, \ 'lnum': 37,
\ 'text': 'warning: some warning', \ 'text': 'warning: some warning',
\ 'type': 'W', \ 'type': 'W',
\ }, \ },
\ { \ {
\ 'filename': '/tmp/vLPr4Q5/33/foo.java', \ 'filename': ale#path#Simplify('/tmp/vLPr4Q5/33/foo.java'),
\ 'lnum': 42, \ 'lnum': 42,
\ 'col': 11, \ 'col': 11,
\ 'text': 'error: cannot find symbol: bar()', \ 'text': 'error: cannot find symbol: bar()',

View file

@ -287,9 +287,9 @@ Execute(The tslint handler should not report no-implicit-dependencies errors):
Execute(The tslint handler should set filename keys for temporary files): Execute(The tslint handler should set filename keys for temporary files):
" The temporay filename below is hacked into being a relative path so we can " The temporay filename below is hacked into being a relative path so we can
" test that we resolve the temporary filename first. " test that we resolve the temporary filename first.
let b:relative_to_root = substitute(expand('%:p'), '\v[^/\\]*([/\\])[^/\\]*', has('win32') ? '..\' : '../', 'g') let b:relative_to_root = substitute(expand('%:p'), '\v[^/\\]*([/\\])[^/\\]*', '../', 'g')
let b:tempname_suffix = substitute(tempname(), '^\v([A-Z]:)?[/\\]', '', '') let b:tempname_suffix = substitute(tempname(), '^\v([A-Z]:)?[/\\]', '', '')
let b:relative_tempname = b:relative_to_root . b:tempname_suffix let b:relative_tempname = substitute(b:relative_to_root . b:tempname_suffix, '\\', '/', 'g')
AssertEqual AssertEqual
\ [ \ [

View file

@ -40,7 +40,7 @@ Execute(The C GCC handler should include 'include' directories for projects with
\ ale#Escape('gcc') \ ale#Escape('gcc')
\ . ' -S -x c -fsyntax-only ' \ . ' -S -x c -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#c#gcc#GetCommand(bufnr('')) \ , ale_linters#c#gcc#GetCommand(bufnr(''))
@ -53,7 +53,7 @@ Execute(The C GCC handler should include 'include' directories for projects with
\ ale#Escape('gcc') \ ale#Escape('gcc')
\ . ' -S -x c -fsyntax-only ' \ . ' -S -x c -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/subdir')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/subdir')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#c#gcc#GetCommand(bufnr('')) \ , ale_linters#c#gcc#GetCommand(bufnr(''))
@ -92,7 +92,7 @@ Execute(The C Clang handler should include 'include' directories for projects wi
\ ale#Escape('clang') \ ale#Escape('clang')
\ . ' -S -x c -fsyntax-only ' \ . ' -S -x c -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#c#clang#GetCommand(bufnr('')) \ , ale_linters#c#clang#GetCommand(bufnr(''))
@ -144,7 +144,7 @@ Execute(The C++ GCC handler should include 'include' directories for projects wi
\ ale#Escape('gcc') \ ale#Escape('gcc')
\ . ' -S -x c++ -fsyntax-only ' \ . ' -S -x c++ -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#cpp#gcc#GetCommand(bufnr('')) \ , ale_linters#cpp#gcc#GetCommand(bufnr(''))
@ -157,7 +157,7 @@ Execute(The C++ GCC handler should include 'include' directories for projects wi
\ ale#Escape('gcc') \ ale#Escape('gcc')
\ . ' -S -x c++ -fsyntax-only ' \ . ' -S -x c++ -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/subdir')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/subdir')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#cpp#gcc#GetCommand(bufnr('')) \ , ale_linters#cpp#gcc#GetCommand(bufnr(''))
@ -196,7 +196,7 @@ Execute(The C++ Clang handler should include 'include' directories for projects
\ ale#Escape('clang++') \ ale#Escape('clang++')
\ . ' -S -x c++ -fsyntax-only ' \ . ' -S -x c++ -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/subdir')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/makefile_project/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#cpp#clang#GetCommand(bufnr('')) \ , ale_linters#cpp#clang#GetCommand(bufnr(''))
@ -209,7 +209,7 @@ Execute(The C++ Clang handler should include 'include' directories for projects
\ ale#Escape('clang++') \ ale#Escape('clang++')
\ . ' -S -x c++ -fsyntax-only ' \ . ' -S -x c++ -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/subdir')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/subdir')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/configure_project/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#cpp#clang#GetCommand(bufnr('')) \ , ale_linters#cpp#clang#GetCommand(bufnr(''))
@ -256,7 +256,7 @@ Execute(The C++ Clang handler shoud use the include directory based on the .git
\ ale#Escape('clang++') \ ale#Escape('clang++')
\ . ' -S -x c++ -fsyntax-only ' \ . ' -S -x c++ -fsyntax-only '
\ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/git_and_nested_makefiles/src')) . ' ' \ . '-iquote ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/git_and_nested_makefiles/src')) . ' '
\ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/git_and_nested_makefiles') . '/include') . ' ' \ . ' -I' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/git_and_nested_makefiles/include')) . ' '
\ . ' -' \ . ' -'
\ , ale_linters#cpp#clang#GetCommand(bufnr('')) \ , ale_linters#cpp#clang#GetCommand(bufnr(''))
@ -268,7 +268,7 @@ Execute(The C++ ClangTidy handler should include json folders for projects with
AssertEqual AssertEqual
\ ale#Escape('clang-tidy') \ ale#Escape('clang-tidy')
\ . ' -checks=' . ale#Escape('*') . ' %s ' \ . ' -checks=' . ale#Escape('*') . ' %s '
\ . '-p ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/json_project') . '/build') \ . '-p ' . ale#Escape(ale#path#Winify(g:dir . '/test_c_projects/json_project/build'))
\ , ale_linters#cpp#clangtidy#GetCommand(bufnr('')) \ , ale_linters#cpp#clangtidy#GetCommand(bufnr(''))
Execute(Move .git/HEAD back): Execute(Move .git/HEAD back):

View file

@ -1,15 +1,29 @@
Execute(Relative paths should be resolved correctly): Execute(Relative paths should be resolved correctly):
AssertEqual AssertEqual
\ '/foo/bar/baz/whatever.txt', \ has('win32') ? '\foo\bar\baz\whatever.txt' : '/foo/bar/baz/whatever.txt',
\ ale#path#GetAbsPath('/foo/bar/xyz', '../baz/whatever.txt') \ ale#path#GetAbsPath('/foo/bar/xyz', '../baz/whatever.txt')
AssertEqual AssertEqual
\ has('win32') ? '/foo/bar/xyz\whatever.txt' : '/foo/bar/xyz/whatever.txt', \ has('win32') ? '\foo\bar\xyz\whatever.txt' : '/foo/bar/xyz/whatever.txt',
\ ale#path#GetAbsPath('/foo/bar/xyz', './whatever.txt') \ ale#path#GetAbsPath('/foo/bar/xyz', './whatever.txt')
AssertEqual AssertEqual
\ has('win32') ? '/foo/bar/xyz\whatever.txt' : '/foo/bar/xyz/whatever.txt', \ has('win32') ? '\foo\bar\xyz\whatever.txt' : '/foo/bar/xyz/whatever.txt',
\ ale#path#GetAbsPath('/foo/bar/xyz', 'whatever.txt') \ ale#path#GetAbsPath('/foo/bar/xyz', 'whatever.txt')
if has('win32')
AssertEqual
\ 'C:\foo\bar\baz\whatever.txt',
\ ale#path#GetAbsPath('C:\foo\bar\baz\xyz', '../whatever.txt')
endif
Execute(Absolute paths should be resolved correctly): Execute(Absolute paths should be resolved correctly):
AssertEqual AssertEqual
\ '/ding/dong', \ has('win32') ? '\ding\dong' : '/ding/dong',
\ ale#path#GetAbsPath('/foo/bar/xyz', '/ding/dong') \ ale#path#GetAbsPath('/foo/bar/xyz', '/ding/dong')
AssertEqual
\ has('win32') ? '\ding\dong' : '/ding/dong',
\ ale#path#GetAbsPath('/foo/bar/xyz', '//ding/dong')
if has('win32')
AssertEqual '\ding', ale#path#GetAbsPath('/foo/bar/xyz', '\\ding')
endif

View file

@ -1,52 +1,48 @@
After: Execute(ale#path#Upwards should return the correct path components):
let g:ale_has_override = {} if has('unix')
" Absolute paths should include / on the end.
AssertEqual
\ ['/foo/bar/baz', '/foo/bar', '/foo', '/'],
\ ale#path#Upwards('/foo/bar/baz')
AssertEqual
\ ['/foo/bar/baz', '/foo/bar', '/foo', '/'],
\ ale#path#Upwards('/foo/bar/baz///')
" Relative paths do not.
AssertEqual
\ ['foo/bar/baz', 'foo/bar', 'foo'],
\ ale#path#Upwards('foo/bar/baz')
AssertEqual
\ ['foo2/bar', 'foo2'],
\ ale#path#Upwards('foo//..////foo2////bar')
" Expect an empty List for empty strings.
AssertEqual [], ale#path#Upwards('')
endif
Execute(ale#path#Upwards should return the correct path components for Unix): if has('win32')
let g:ale_has_override = {'win32': 0} AssertEqual
\ ['C:\foo\bar\baz', 'C:\foo\bar', 'C:\foo', 'C:\'],
" Absolute paths should include / on the end. \ ale#path#Upwards('C:\foo\bar\baz')
AssertEqual AssertEqual
\ ['/foo/bar/baz', '/foo/bar', '/foo', '/'], \ ['C:\foo\bar\baz', 'C:\foo\bar', 'C:\foo', 'C:\'],
\ ale#path#Upwards('/foo/bar/baz') \ ale#path#Upwards('C:\foo\bar\baz\\\')
AssertEqual AssertEqual
\ ['/foo/bar/baz', '/foo/bar', '/foo', '/'], \ ['/foo\bar\baz', '/foo\bar', '/foo', '/'],
\ ale#path#Upwards('/foo/bar/baz///') \ ale#path#Upwards('/foo/bar/baz')
" Relative paths do not. AssertEqual
AssertEqual \ ['foo\bar\baz', 'foo\bar', 'foo'],
\ ['foo/bar/baz', 'foo/bar', 'foo'], \ ale#path#Upwards('foo/bar/baz')
\ ale#path#Upwards('foo/bar/baz') AssertEqual
AssertEqual \ ['foo\bar\baz', 'foo\bar', 'foo'],
\ ['foo2/bar', 'foo2'], \ ale#path#Upwards('foo\bar\baz')
\ ale#path#Upwards('foo//..////foo2////bar') " simplify() is used internally, and should sort out \ paths when actually
" Expect an empty List for empty strings. " running Windows, which we can't test here.
AssertEqual [], ale#path#Upwards('') AssertEqual
\ ['foo2\bar', 'foo2'],
Execute(ale#path#Upwards should return the correct path components for Windows): \ ale#path#Upwards('foo//..///foo2////bar')
let g:ale_has_override = {'win32': 1} " Expect an empty List for empty strings.
AssertEqual [], ale#path#Upwards('')
AssertEqual " Paths starting with // return /
\ ['C:\foo\bar\baz', 'C:\foo\bar', 'C:\foo', 'C:\'], AssertEqual
\ ale#path#Upwards('C:\foo\bar\baz') \ ['/foo2\bar', '/foo2', '/'],
AssertEqual \ ale#path#Upwards('//foo//..///foo2////bar')
\ ['C:\foo\bar\baz', 'C:\foo\bar', 'C:\foo', 'C:\'], endif
\ ale#path#Upwards('C:\foo\bar\baz\\\')
AssertEqual
\ ['/foo\bar\baz', '/foo\bar', '/foo', '/'],
\ ale#path#Upwards('/foo/bar/baz')
AssertEqual
\ ['foo\bar\baz', 'foo\bar', 'foo'],
\ ale#path#Upwards('foo/bar/baz')
AssertEqual
\ ['foo\bar\baz', 'foo\bar', 'foo'],
\ ale#path#Upwards('foo\bar\baz')
" simplify() is used internally, and should sort out \ paths when actually
" running Windows, which we can't test here.
AssertEqual
\ ['foo2\bar', 'foo2'],
\ ale#path#Upwards('foo//..///foo2////bar')
" Expect an empty List for empty strings.
AssertEqual [], ale#path#Upwards('')
" Paths starting with // return /
AssertEqual
\ ['/foo2\bar', '/foo2', '/'],
\ ale#path#Upwards('//foo//..///foo2////bar')