From 0856c8bf5101105beaf7f58b77c9e3870d95141c Mon Sep 17 00:00:00 2001 From: micbou Date: Sat, 25 Aug 2018 01:34:36 +0200 Subject: [PATCH] Add system header directories to user flags --- .gitignore | 3 + CORE_VERSION | 2 +- build.py | 20 +-- cpp/ycm/CMakeLists.txt | 12 +- cpp/ycm/fake_clang/CMakeLists.txt | 38 +++++ cpp/ycm/fake_clang/main.cpp | 42 ++++++ ycmd/completers/cpp/flags.py | 234 +++++++++++++++++------------- ycmd/tests/clang/flags_test.py | 225 +++++++++++++++------------- 8 files changed, 357 insertions(+), 219 deletions(-) create mode 100644 cpp/ycm/fake_clang/CMakeLists.txt create mode 100644 cpp/ycm/fake_clang/main.cpp 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.' ) + )