diff --git a/.gitignore b/.gitignore
index 6af43f11e0..4d8b05903c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,9 @@
*.la
*.a
+# Executables
+ycm_fake_clang*
+
# Clang archives
clang_archives
diff --git a/CORE_VERSION b/CORE_VERSION
index 87523dd7a0..d81cc0710e 100644
--- a/CORE_VERSION
+++ b/CORE_VERSION
@@ -1 +1 @@
-41
+42
diff --git a/build.py b/build.py
index 26efe9a1a4..04da02ec63 100755
--- a/build.py
+++ b/build.py
@@ -538,22 +538,12 @@ def BuildYcmdLib( cmake, cmake_common_args, script_args ):
quiet = script_args.quiet,
status_message = 'Generating ycmd build configuration' )
- build_targets = [ 'ycm_core' ]
- if script_args.core_tests:
- build_targets.append( 'ycm_core_tests' )
- if 'YCM_BENCHMARK' in os.environ:
- build_targets.append( 'ycm_core_benchmarks' )
-
build_config = GetCMakeBuildConfiguration( script_args )
-
- for target in build_targets:
- build_command = ( [ cmake, '--build', '.', '--target', target ] +
- build_config )
- CheckCall( build_command,
- exit_message = BUILD_ERROR_MESSAGE,
- quiet = script_args.quiet,
- status_message = 'Compiling ycmd target: {0}'.format(
- target ) )
+ build_command = [ cmake, '--build', '.' ] + build_config
+ CheckCall( build_command,
+ exit_message = BUILD_ERROR_MESSAGE,
+ quiet = script_args.quiet,
+ status_message = 'Compiling ycmd' )
if script_args.core_tests:
RunYcmdTests( script_args, build_dir )
diff --git a/cpp/ycm/CMakeLists.txt b/cpp/ycm/CMakeLists.txt
index b0dc892ee9..9f13159836 100644
--- a/cpp/ycm/CMakeLists.txt
+++ b/cpp/ycm/CMakeLists.txt
@@ -204,11 +204,10 @@ set( PYBIND11_INCLUDES_DIR "${CMAKE_SOURCE_DIR}/pybind11" )
file( GLOB_RECURSE SERVER_SOURCES *.h *.cpp )
-# The test and benchmark sources are a part of a different target, so we remove
-# them. The CMakeFiles cpp file is picked up when the user creates an in-source
-# build, and we don't want that. We also remove client-specific code.
-file( GLOB_RECURSE to_remove tests/*.h tests/*.cpp benchmarks/*.h
- benchmarks/*.cpp CMakeFiles/*.cpp *client* )
+# The tests, benchmarks, and fake_clang sources are a part of a different
+# target, so we remove them. The CMakeFiles cpp file is picked up when the user
+# creates an in-source build, and we don't want that.
+file( GLOB_RECURSE to_remove tests/* benchmarks/* fake_clang/* CMakeFiles/* )
if( to_remove )
list( REMOVE_ITEM SERVER_SOURCES ${to_remove} )
@@ -444,6 +443,9 @@ if( SYSTEM_IS_OPENBSD OR SYSTEM_IS_FREEBSD )
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread" )
endif()
+if ( USE_CLANG_COMPLETER )
+ add_subdirectory( fake_clang )
+endif()
if ( DEFINED ENV{YCM_TESTRUN} )
add_subdirectory( tests )
endif()
diff --git a/cpp/ycm/fake_clang/CMakeLists.txt b/cpp/ycm/fake_clang/CMakeLists.txt
new file mode 100644
index 0000000000..5bad02d7a6
--- /dev/null
+++ b/cpp/ycm/fake_clang/CMakeLists.txt
@@ -0,0 +1,38 @@
+# Copyright (C) 2018 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd. If not, see .
+
+project( ycm_fake_clang )
+cmake_minimum_required( VERSION 2.8 )
+
+include_directories( ${ycm_core_SOURCE_DIR} )
+
+add_executable( ${PROJECT_NAME} main.cpp )
+
+target_link_libraries( ${PROJECT_NAME}
+ ${LIBCLANG_TARGET} )
+
+# Build ycm_fake_clang in ycmd root folder.
+if ( MSVC )
+ foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} )
+ string( TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG )
+ set_target_properties( ${PROJECT_NAME} PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${PROJECT_SOURCE_DIR}/../../.. )
+ endforeach()
+else()
+ set_target_properties( ${PROJECT_NAME} PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/../../.. )
+endif()
diff --git a/cpp/ycm/fake_clang/main.cpp b/cpp/ycm/fake_clang/main.cpp
new file mode 100644
index 0000000000..e509235937
--- /dev/null
+++ b/cpp/ycm/fake_clang/main.cpp
@@ -0,0 +1,42 @@
+// Copyright (C) 2018 ycmd contributors
+//
+// This file is part of ycmd.
+//
+// ycmd is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// ycmd is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with ycmd. If not, see .
+
+#include
+#include
+
+// This small program simulates the output of the clang executable when ran with
+// the -E and -v flags. It takes a list of flags as arguments and creates the
+// corresponding translation unit. When retrieving user flags, ycmd executes
+// this program as follows
+//
+// ycm_fake_clang -resource-dir=... [flag ...] -E -v filename
+//
+// and extract the list of system header paths from the output. These
+// directories are then added to the list of flags to provide completion of
+// system headers in include statements and allow jumping to these headers.
+int main( int argc, char **argv ) {
+ CXIndex index = clang_createIndex( 0, 0 );
+ CXTranslationUnit tu;
+ CXErrorCode result = clang_parseTranslationUnit2FullArgv(
+ index, nullptr, argv, argc, nullptr, 0, CXTranslationUnit_None, &tu );
+ if ( result != CXError_Success ) {
+ return EXIT_FAILURE;
+ }
+
+ clang_disposeTranslationUnit( tu );
+ return EXIT_SUCCESS;
+}
diff --git a/ycmd/completers/cpp/flags.py b/ycmd/completers/cpp/flags.py
index 09179fa357..3a4d71c093 100644
--- a/ycmd/completers/cpp/flags.py
+++ b/ycmd/completers/cpp/flags.py
@@ -22,21 +22,27 @@
# Not installing aliases from python-future; it's unreliable and slow.
from builtins import * # noqa
-import ycm_core
-import os
import inspect
+import logging
+import os
+import subprocess
+import tempfile
+import ycm_core
from future.utils import PY2, native
from ycmd import extra_conf_store
-from ycmd.utils import ( ListDirectory,
- OnMac,
+from ycmd.utils import ( FindExecutable,
+ GetExecutable,
OnWindows,
PathsToAllParentFolders,
re,
- ToCppStringCompatible,
+ SafePopen,
ToBytes,
+ ToCppStringCompatible,
ToUnicode )
from ycmd.responses import NoExtraConfDetected
+_logger = logging.getLogger( __name__ )
+
# -include-pch and --sysroot= must be listed before -include and --sysroot
# respectively because the latter is a prefix of the former (and the algorithm
# checks prefixes).
@@ -86,6 +92,22 @@
'flags': [],
}
+PATH_TO_YCMD_DIR = os.path.abspath( os.path.dirname( ycm_core.__file__ ) )
+CLANG_RESOURCE_DIR = '-resource-dir=' + os.path.join( PATH_TO_YCMD_DIR,
+ 'clang_includes' )
+
+REAL_CLANG_EXECUTABLE = FindExecutable( 'clang' )
+FAKE_CLANG_EXECUTABLE = GetExecutable( os.path.join( PATH_TO_YCMD_DIR,
+ 'ycm_fake_clang' ) )
+
+# Regular expression to capture the list of system headers from the output of
+# clang -E -v.
+SYSTEM_HEADER_REGEX = re.compile(
+ "#include <\.\.\.> search starts here:\r?\n"
+ "((?: .*\r?\n)*)"
+ "End of search list.",
+ re.MULTILINE )
+
class Flags( object ):
"""Keeps track of the flags necessary to compile a file.
@@ -157,8 +179,8 @@ def _ParseFlagsFromExtraConfOrDatabase( self,
return [], filename
if add_extra_clang_flags:
+ flags = _AddSystemHeaderPaths( flags, filename )
flags += self.extra_clang_flags
- flags = _AddMacIncludePaths( flags )
sanitized_flags = PrepareFlagsForClang( flags,
filename,
@@ -317,14 +339,6 @@ def _CallExtraConfFlagsForFile( module, filename, client_data ):
return results
-def _SysRootSpecifedIn( flags ):
- for flag in flags:
- if flag == '-isysroot' or flag.startswith( '--sysroot' ):
- return True
-
- return False
-
-
def PrepareFlagsForClang( flags,
filename,
add_extra_clang_flags = True,
@@ -505,87 +519,8 @@ def _SkipStrayFilenameFlag( current_flag,
( not previous_flag_is_include and current_flag_may_be_path ) ) )
-# Return the path to the macOS toolchain root directory to use for system
-# includes. If no toolchain is found, returns None.
-def _SelectMacToolchain():
- # There are 2 ways to get a development enviornment (as standard) on OS X:
- # - install XCode.app, or
- # - install the command-line tools (xcode-select --install)
- #
- # Most users have xcode installed, but in order to be as compatible as
- # possible we consider both possible installation locations
- MAC_CLANG_TOOLCHAIN_DIRS = [
- '/Applications/Xcode.app/Contents/Developer/Toolchains/'
- 'XcodeDefault.xctoolchain',
- '/Library/Developer/CommandLineTools'
- ]
-
- for toolchain in MAC_CLANG_TOOLCHAIN_DIRS:
- if os.path.exists( toolchain ):
- return toolchain
-
- return None
-
-
-# Return the list of flags including any Clang headers found in the supplied
-# toolchain. These are required for the same reasons as described below, but
-# unfortunately, these are in versioned directories and there is no easy way to
-# find the "correct" version. We simply pick the highest version in the first
-# toolchain that we find, as this is the most likely to be correct.
-def _LatestMacClangIncludes( toolchain ):
- # We use the first toolchain which actually contains any versions, rather than
- # trying all of the toolchains and picking the highest. We favour Xcode over
- # CommandLineTools as using Xcode is more common. It might be possible to
- # extract this information from xcode-select, though xcode-select -p does not
- # point at the toolchain directly.
- candidates_dir = os.path.join( toolchain, 'usr', 'lib', 'clang' )
- versions = ListDirectory( candidates_dir )
-
- for version in reversed( sorted( versions ) ):
- candidate_include = os.path.join( candidates_dir, version, 'include' )
- if os.path.exists( candidate_include ):
- return [ '-isystem', candidate_include ]
-
- return []
-
-
-MAC_INCLUDE_PATHS = []
-
-if OnMac():
- # These are the standard header search paths that clang will use on Mac BUT
- # libclang won't, for unknown reasons. We add these paths when the user is on
- # a Mac because if we don't, libclang would fail to find etc. This
- # should be fixed upstream in libclang, but until it does, we need to help
- # users out.
- # See the following for details:
- # - Valloric/YouCompleteMe#303
- # - Valloric/YouCompleteMe#2268
- toolchain = _SelectMacToolchain()
- if toolchain:
- MAC_INCLUDE_PATHS = (
- [ '-isystem', os.path.join( toolchain, 'usr/include/c++/v1' ),
- '-isystem', '/usr/local/include' ] +
- _LatestMacClangIncludes( toolchain ) +
- [ '-isystem', os.path.join( toolchain, 'usr/include' ),
- '-isystem', '/usr/include',
- '-iframework', '/System/Library/Frameworks',
- '-iframework', '/Library/Frameworks',
- # We include the MacOS platform SDK because some meaningful parts of the
- # standard library are located there. If users are compiling for (say)
- # iPhone.platform, etc. they should appear earlier in the include path.
- '-isystem', '/Applications/Xcode.app/Contents/Developer/Platforms'
- '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include' ]
- )
-
-
-def _AddMacIncludePaths( flags ):
- if OnMac() and not _SysRootSpecifedIn( flags ):
- flags.extend( MAC_INCLUDE_PATHS )
- return flags
-
-
def _ExtraClangFlags():
- flags = _SpecialClangIncludes()
+ flags = [ CLANG_RESOURCE_DIR ]
# On Windows, parsing of templates is delayed until instantiation time.
# This makes GetType and GetParent commands fail to return the expected
# result when the cursor is in a template.
@@ -618,12 +553,6 @@ def _EnableTypoCorrection( flags ):
return flags
-def _SpecialClangIncludes():
- libclang_dir = os.path.dirname( ycm_core.__file__ )
- path_to_includes = os.path.join( libclang_dir, 'clang_includes' )
- return [ '-resource-dir=' + path_to_includes ]
-
-
def _MakeRelativePathsInFlagsAbsolute( flags, working_directory ):
if not working_directory:
return list( flags )
@@ -728,3 +657,108 @@ def UserIncludePaths( user_flags, filename ):
pass
return quoted_include_paths, include_paths, framework_paths
+
+
+def _GetSystemFlags( flags ):
+ """Return the flags from |flags| that are relevant to the system header
+ directories returned by Clang:
+ - the --cuda-path flag which specifies the CUDA installation path;
+ - the -gcc-toolchain flag for using a particular GCC toolchain;
+ - the -nocudainc for not including the CUDA headers;
+ - the -nostdinc and -nostdinc++ flags for not including the system header
+ directories;
+ - the -stdlib flag for using the libc++ standard library;
+ - the --sysroot flag which specifies the headers and libraries root folder;
+ - the -target flag which specifies the target;
+ - the -x flag which determines the language used to parse the translation
+ unit.
+ Return also a boolean that is true if no -x flag is given."""
+ # Clang may return an error instead of printing the list of system header
+ # directories for CUDA if -nocudalib is not given. This flag is ignored for
+ # other languages so it's safe to always add it.
+ system_flags = [ '-nocudalib' ]
+ no_language_flag = True
+
+ try:
+ iter_flags = iter( flags )
+ for flag in iter_flags:
+ if flag == '-x':
+ system_flags.extend( [ flag, next( iter_flags ) ] )
+ no_language_flag = False
+ continue
+
+ if flag in [ '-gcc-toolchain', '--stdlib', '--sysroot', '-target' ]:
+ system_flags.extend( [ flag, next( iter_flags ) ] )
+ continue
+
+ if flag in [ '-nocudainc', '-nostdinc', '-nostdinc++' ]:
+ system_flags.append( flag )
+ continue
+
+ if flag.startswith( '-x' ):
+ system_flags.append( flag )
+ no_language_flag = False
+ continue
+
+ for start in [ '--cuda-path=',
+ '--gcc-toolchain=',
+ '-stdlib=', '--stdlib=',
+ '--sysroot=',
+ '--target=' ]:
+ if flag.startswith( start ):
+ system_flags.append( flag )
+ continue
+ except StopIteration:
+ pass
+
+ return system_flags, no_language_flag
+
+
+def _AddSystemHeaderPaths( flags, filename ):
+ """Add the system header directories to the list of flags given by the user.
+ This is needed to provide completion of these headers in include statements
+ as well as jumping to these headers."""
+
+ # Use Clang or the ycm_fake_clang executable to output the list of system
+ # header directories. If no executable is found, ycmd was not properly
+ # compiled so raise an error.
+ if REAL_CLANG_EXECUTABLE:
+ clang_command = [ REAL_CLANG_EXECUTABLE ]
+ elif FAKE_CLANG_EXECUTABLE:
+ clang_command = [ FAKE_CLANG_EXECUTABLE, CLANG_RESOURCE_DIR ]
+ else:
+ raise RuntimeError( 'No Clang executable found.' )
+
+ # Create a temporary file with the same file extension as the input one; Clang
+ # deduces the language from the extension when the -x flag is not given.
+ _, extension = os.path.splitext( filename )
+ system_flags, no_language_flag = _GetSystemFlags( flags )
+ # Clang cannot parse the file if it has no extension and no language flag -x
+ # is given. Force the language to C++ in that case as it's most likely a
+ # header from the STL.
+ if not extension and no_language_flag:
+ flags.extend( [ '-x', 'c++' ] )
+ system_flags.extend( [ '-x', 'c++' ] )
+ with tempfile.NamedTemporaryFile( suffix = extension ) as temp_file:
+ clang_command.extend( system_flags + [ '-E', '-v', temp_file.name ] )
+ _, stderr = SafePopen( clang_command,
+ stdout = subprocess.PIPE,
+ stderr = subprocess.PIPE ).communicate()
+
+ match = re.search( SYSTEM_HEADER_REGEX, ToUnicode( stderr ) )
+ if not match:
+ _logger.error( 'Unable to parse system header directories from output '
+ '%s returned by the command %s', stderr, clang_command )
+ raise RuntimeError( 'Unable to parse system header directories '
+ 'from Clang output.' )
+
+ system_headers = []
+ for include_line in match.group( 1 ).splitlines():
+ include_line = include_line.strip()
+ if include_line.endswith( ' (framework directory)' ):
+ framework_path = include_line[ : -len( ' (framework directory)' ) ]
+ system_headers.extend( [ '-iframework',
+ os.path.abspath( framework_path ) ] )
+ else:
+ system_headers.extend( [ '-isystem', os.path.abspath( include_line ) ] )
+ return flags + system_headers
diff --git a/ycmd/tests/clang/flags_test.py b/ycmd/tests/clang/flags_test.py
index 56943b515c..b101abd5c5 100644
--- a/ycmd/tests/clang/flags_test.py
+++ b/ycmd/tests/clang/flags_test.py
@@ -24,24 +24,17 @@
import contextlib
import os
-
+from hamcrest import assert_that, calling, contains, empty, equal_to, raises
from nose.tools import eq_, ok_
-from ycmd.completers.cpp import flags
from mock import patch, MagicMock
from types import ModuleType
-from ycmd.tests.test_utils import MacOnly, TemporaryTestDir, WindowsOnly
+
+from ycmd import utils
+from ycmd.completers.cpp import flags
+from ycmd.completers.cpp.flags import _ShouldAllowWinStyleFlags
from ycmd.responses import NoExtraConfDetected
from ycmd.tests.clang import TemporaryClangProject
-from ycmd.completers.cpp.flags import _ShouldAllowWinStyleFlags
-
-from hamcrest import ( assert_that,
- calling,
- contains,
- empty,
- equal_to,
- has_item,
- not_,
- raises )
+from ycmd.tests.test_utils import TemporaryTestDir, WindowsOnly
@contextlib.contextmanager
@@ -193,56 +186,6 @@ def Settings( **kwargs ):
'-I', os.path.normpath( '/working_dir/header' ) ) )
-@MacOnly
-@patch( 'ycmd.completers.cpp.flags.MAC_INCLUDE_PATHS',
- [ 'sentinel_value_for_testing' ] )
-def FlagsForFile_AddMacIncludePathsWithoutSysroot_test():
- flags_object = flags.Flags()
-
- def Settings( **kwargs ):
- return {
- 'flags': [ '-test', '--test1', '--test2=test' ]
- }
-
- with MockExtraConfModule( Settings ):
- flags_list, _ = flags_object.FlagsForFile( '/foo' )
- assert_that( flags_list, has_item( 'sentinel_value_for_testing' ) )
-
-
-@MacOnly
-@patch( 'ycmd.completers.cpp.flags.MAC_INCLUDE_PATHS',
- [ 'sentinel_value_for_testing' ] )
-def FlagsForFile_DoNotAddMacIncludePathsWithSysroot_test():
- flags_object = flags.Flags()
-
- def Settings( **kwargs ):
- return {
- 'flags': [ '-isysroot', 'test1', '--test2=test' ]
- }
-
- with MockExtraConfModule( Settings ):
- flags_list, _ = flags_object.FlagsForFile( '/foo' )
- assert_that( flags_list, not_( has_item( 'sentinel_value_for_testing' ) ) )
-
- def Settings( **kwargs ):
- return {
- 'flags': [ '-test', '--sysroot', 'test1' ]
- }
-
- with MockExtraConfModule( Settings ):
- flags_list, _ = flags_object.FlagsForFile( '/foo' )
- assert_that( flags_list, not_( has_item( 'sentinel_value_for_testing' ) ) )
-
- def Settings( **kwargs ):
- return {
- 'flags': [ '-test', 'test1', '--sysroot=test' ]
- }
-
- with MockExtraConfModule( Settings ):
- flags_list, _ = flags_object.FlagsForFile( '/foo' )
- assert_that( flags_list, not_( has_item( 'sentinel_value_for_testing' ) ) )
-
-
def FlagsForFile_OverrideTranslationUnit_test():
flags_object = flags.Flags()
@@ -809,41 +752,6 @@ def ExtraClangFlags_test():
eq_( 1, num_found )
-@MacOnly
-@patch( 'os.listdir',
- return_value = [ '1.0.0', '7.0.1', '7.0.2', '___garbage__' ] )
-@patch( 'os.path.exists', side_effect = [ False, True, True, True ] )
-def Mac_LatestMacClangIncludes_test( *args ):
- eq_( flags._LatestMacClangIncludes( '/tmp' ),
- [ '-isystem', '/tmp/usr/lib/clang/7.0.2/include' ] )
-
-
-@MacOnly
-@patch( 'os.listdir', side_effect = OSError )
-def Mac_LatestMacClangIncludes_NoSuchDirectory_test( *args ):
- eq_( flags._LatestMacClangIncludes( '/tmp' ), [] )
-
-
-@MacOnly
-@patch( 'os.path.exists', side_effect = [ False, False ] )
-def Mac_SelectMacToolchain_None_test( *args ):
- eq_( flags._SelectMacToolchain(), None )
-
-
-@MacOnly
-@patch( 'os.path.exists', side_effect = [ True, False ] )
-def Mac_SelectMacToolchain_XCode_test( *args ):
- eq_( flags._SelectMacToolchain(),
- '/Applications/Xcode.app/Contents/Developer/Toolchains/'
- 'XcodeDefault.xctoolchain' )
-
-
-@MacOnly
-@patch( 'os.path.exists', side_effect = [ False, True ] )
-def Mac_SelectMacToolchain_CommandLineTools_test( *args ):
- eq_( flags._SelectMacToolchain(), '/Library/Developer/CommandLineTools' )
-
-
def CompilationDatabase_NoDatabase_test():
with TemporaryTestDir() as tmp_dir:
assert_that(
@@ -1341,3 +1249,124 @@ def MakeRelativePathsInFlagsAbsolute_NoWorkingDir_test():
'expect': [ 'list', 'of', 'flags', 'not', 'changed', '-Itest' ],
'wd': ''
}
+
+
+def GetSystemFlags( user_flags, expected_flags, expected_no_language_flag ):
+ assert_that( flags._GetSystemFlags( user_flags ),
+ contains( contains( *expected_flags ),
+ expected_no_language_flag ) )
+
+
+def GetSystemFlags_test():
+ tests = [
+ ( [ '-x' ], [ '-nocudalib' ], True ),
+ ( [ '-foo', '-bar' ], [ '-nocudalib' ], True ),
+ ( [ '-x', 'c++' ], [ '-nocudalib', '-x', 'c++' ], False ),
+ ( [ '-xc' ], [ '-nocudalib', '-xc' ], False ),
+ ( [ '--cuda-path=/foo' ],
+ [ '-nocudalib', '--cuda-path=/foo' ], True ),
+ ( [ '-gcc-toolchain', '/foo' ],
+ [ '-nocudalib', '-gcc-toolchain', '/foo' ], True ),
+ ( [ '--gcc-toolchain=/foo' ],
+ [ '-nocudalib', '--gcc-toolchain=/foo' ], True ),
+ ( [ '-nocudainc' ], [ '-nocudalib', '-nocudainc' ], True ),
+ ( [ '-nostdinc' ], [ '-nocudalib', '-nostdinc' ], True ),
+ ( [ '-nostdinc++' ], [ '-nocudalib', '-nostdinc++' ], True ),
+ ( [ '--stdlib', 'libc++' ],
+ [ '-nocudalib', '--stdlib', 'libc++' ], True ),
+ ( [ '-stdlib=libc++' ], [ '-nocudalib', '-stdlib=libc++' ], True ),
+ ( [ '--stdlib=libc++' ],
+ [ '-nocudalib', '--stdlib=libc++' ], True ),
+ ( [ '--sysroot', '/foo' ],
+ [ '-nocudalib', '--sysroot', '/foo' ], True ),
+ ( [ '--sysroot=/foo' ], [ '-nocudalib', '--sysroot=/foo' ], True ),
+ ( [ '-target', 'foo' ], [ '-nocudalib', '-target', 'foo' ], True ),
+ ( [ '--target=foo' ], [ '-nocudalib', '--target=foo' ], True ),
+ ( [ '-foo', '-x', 'c', '-bar', '--target=/foo', '-wyz' ],
+ [ '-nocudalib', '-x', 'c', '--target=/foo' ], False )
+ ]
+
+ for test in tests:
+ yield GetSystemFlags, test[ 0 ], test[ 1 ], test[ 2 ]
+
+
+FAKE_CLANG_STDERR = """
+#include "..." search starts here:
+#include <...> search starts here:
+ /path/to/include
+ /path/to/framework (framework directory)
+End of search list."""
+
+
+@patch( 'ycmd.completers.cpp.flags.REAL_CLANG_EXECUTABLE', '/usr/bin/clang' )
+def AddSystemHeaderPaths_ParseDirectoriesFromRealClang_test():
+ class SafePopen( object ):
+ def __init__( self, args, **kwargs ):
+ pass
+
+ def communicate( self ):
+ return '', FAKE_CLANG_STDERR
+
+ with patch( 'ycmd.completers.cpp.flags.SafePopen', SafePopen ):
+ assert_that(
+ flags._AddSystemHeaderPaths( [ '-x', 'c++' ], 'test.cpp' ),
+ contains( '-x', 'c++',
+ '-isystem', os.path.abspath( '/path/to/include' ),
+ '-iframework', os.path.abspath( '/path/to/framework' ) )
+ )
+
+
+@patch( 'ycmd.completers.cpp.flags.REAL_CLANG_EXECUTABLE', None )
+def AddSystemHeaderPaths_ParseDirectoriesFromFakeClang_test():
+ class SafePopen( object ):
+ def __init__( self, args, **kwargs ):
+ utils.SafePopen( args, **kwargs ).communicate()
+
+ def communicate( self ):
+ return '', FAKE_CLANG_STDERR
+
+ with patch( 'ycmd.completers.cpp.flags.SafePopen', SafePopen ):
+ assert_that(
+ flags._AddSystemHeaderPaths( [ '-x', 'c++' ], 'test.cpp' ),
+ contains( '-x', 'c++',
+ '-isystem', os.path.abspath( '/path/to/include' ),
+ '-iframework', os.path.abspath( '/path/to/framework' ) )
+ )
+
+
+@patch( 'ycmd.completers.cpp.flags.REAL_CLANG_EXECUTABLE', None )
+def AddSystemHeaderPaths_AssumeCppIfNoExtensionAndNoLanguageFlag_test():
+ class SafePopen( object ):
+ def __init__( self, args, **kwargs ):
+ utils.SafePopen( args, **kwargs ).communicate()
+
+ def communicate( self ):
+ return '', FAKE_CLANG_STDERR
+
+ with patch( 'ycmd.completers.cpp.flags.SafePopen', SafePopen ):
+ assert_that(
+ flags._AddSystemHeaderPaths( [], 'vector' ),
+ contains( '-x', 'c++',
+ '-isystem', os.path.abspath( '/path/to/include' ),
+ '-iframework', os.path.abspath( '/path/to/framework' ) )
+ )
+
+
+@patch( 'ycmd.completers.cpp.flags.REAL_CLANG_EXECUTABLE', None )
+@patch( 'ycmd.completers.cpp.flags.FAKE_CLANG_EXECUTABLE', None )
+def AddSystemHeaderPaths_NoClangExecutable_test():
+ assert_that(
+ calling( flags._AddSystemHeaderPaths ).with_args( [], 'test.cpp' ),
+ raises( RuntimeError, 'No Clang executable found.' )
+ )
+
+
+@patch( 'ycmd.completers.cpp.flags.REAL_CLANG_EXECUTABLE', None )
+def AddSystemHeaderPaths_RaiseErrorIfUnableToParseClang_test():
+ # Clang does not print the system headers if no language flag is given and the
+ # extension is unknown.
+ assert_that(
+ calling( flags._AddSystemHeaderPaths ).with_args( [], 'test.foo' ),
+ raises( RuntimeError,
+ 'Unable to parse system header directories from Clang output.' )
+ )