Instantly share code, notes, and snippets.

Embed
What would you like to do?
A Python script used by https://github.com/Valloric/ycmd to provide code completion in header files.
#!/bin/env python
# Copyright Nils Deppe, 2017
# Distributed under the Boost Software License - Version 1.0
# Boost Software License - Version 1.0 - August 17th, 2003
# Permission is hereby granted, free of charge, to any person or organization
# obtaining a copy of the software and accompanying documentation covered by
# this license (the "Software") to use, reproduce, display, distribute,
# execute, and transmit the Software, and to prepare derivative works of the
# Software, and to permit third-parties to whom the Software is furnished to
# do so, all subject to the following:
# The copyright notices in the Software and this entire statement, including
# the above license grant, this restriction and the following disclaimer,
# must be included in all copies of the Software, in whole or in part, and
# all derivative works of the Software, unless such copies or derivative
# works are solely in the form of machine-executable object code generated by
# a source language processor.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
# SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
# FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
import os
import ycm_core
default_flags = ['-x', 'c++', '-Wall', '-Wextra', '-Werror', '-std=c++14']
cpp_source_extensions = ['.cpp', '.cxx', '.cc', '.c', '.m', '.mm']
header_file_extensions = ['.h', '.H', '.hxx', '.hpp', '.hh']
completion_database = []
def FindCompilationDatabaseAndDir(my_dir):
"""
Returns the directory the ycm compilation database was found in,
and the database. Returns None if no compilation database was found.
This assumes that any compilation database found in the hierarchy is
the one we want. That is, it assumes projects are not nested.
"""
while my_dir != '/':
if os.path.isfile(os.path.join(my_dir, "compile_commands.json")):
return [my_dir, ycm_core.CompilationDatabase(my_dir)]
my_dir = os.path.dirname(my_dir)
return None
def ReloadCompilationDatabase(my_dir):
"""
Returns the compilation database at 'my_dir/compile_commands.json'
"""
if os.path.isfile(os.path.join(my_dir, "compile_commands.json")):
return ycm_core.CompilationDatabase(my_dir)
return None
def IsHeaderFile(filename):
"""
Returns true if the file has a header file extension
"""
extension = os.path.splitext(filename)[1]
return extension in header_file_extensions
def GetAnyCompilationInfo(dirname, db):
"""
Returns the compilation info for some file in the directory
dirname
"""
source_files_in_dir = list(filter(
(lambda name: os.path.splitext(name)[1] in cpp_source_extensions),
os.listdir(dirname)))
if source_files_in_dir:
filename = dirname + '/' + source_files_in_dir[0]
compilation_info = db.GetCompilationInfoForFile(filename)
if compilation_info.compiler_flags_:
print("Getting flags for a header file, "
"returning compilation flags for file: ", filename)
return compilation_info
return None
def TraverseSubdirsExcept(dir_to_traverse, not_this_dir, db,
db_root_dir='',
jump_up_dir_if_failed=False):
"""
Traverses all sub directories searching for compilation flags.
If jump_up_dir_if_failed is True then the next higher level in
the directory hierarchy is searched as long is it is still in
the project.
"""
subdirs = [os.path.abspath(dir_to_traverse + '/' + x)
for x in next(os.walk(dir_to_traverse))[1]]
if os.path.abspath(not_this_dir) in subdirs:
subdirs.remove(os.path.abspath(not_this_dir))
# Try find compilation flags in each subdir, so a breadth first
# search
for subdir in subdirs:
compilation_info = GetAnyCompilationInfo(subdir, db)
if not compilation_info is None:
return compilation_info
# If we failed to find any flags in the subdirs then:
# For each subdir, search its subdirs
for subdir in subdirs:
compilation_info = TraverseSubdirsExcept(subdir, '', db)
if not compilation_info is None:
return compilation_info
# If requested we go up a directory unless we are already at the
# project root directory/where the compile_commands.json file is.
if jump_up_dir_if_failed:
if db_root_dir not in os.path.split(dir_to_traverse)[0]:
print("ERROR: Failed to find any compilation flags for a "
"header file. The compilation database directory is "
"\n %s" % (db_root_dir))
return None
# Try getting flags from one directory up, otherwise traverse
# directories
return GetAnyCompilationInfo( os.path.split(dir_to_traverse)[0], db) \
or TraverseSubdirsExcept(
os.path.split(dir_to_traverse)[0],
dir_to_traverse, db,
db_root_dir,
jump_up_dir_if_failed)
return None
def GetCompilationInfoForFile(filename, dir_of_db, db):
"""
The compilation_commands.json file generated by CMake does not have entries
for header files. So we do our best by asking the db for flags for a
corresponding source file, if any. If one exists, the flags for that file
should be good enough.
"""
if not IsHeaderFile(filename):
return db.GetCompilationInfoForFile(filename)
# Try to find flags for header files so we get completion there
basename = os.path.splitext(filename)[0]
# Check all possible cpp extensions to see if a file exists with that
# extension
for extension in cpp_source_extensions:
replacement_file = basename + extension
if os.path.exists(replacement_file):
compilation_info = db.GetCompilationInfoForFile(
replacement_file)
if compilation_info.compiler_flags_:
return compilation_info
# If we couldn't find an entry with a source file extension instead
# of a header file extension we find any source file in the directory
# and use those flags, since they should still be quite close to what
# we need.
# or
# Traverse all sub directories first searching for compilation flags.
# If none are found we start searching upward in the directory tree
# to find flags. At each level we search all sub directories that we
# haven't already searched. This is exhaustive and is nearly
# guaranteed to find useful compilation flags.
return GetAnyCompilationInfo(os.path.split(filename)[0], db) \
or TraverseSubdirsExcept(os.path.split(filename)[0],
'',
db,
dir_of_db,
True)
def FlagsForFile(filename, **_):
"""
Returns the flags needed to compile the file. If a compilation database
exists then the flags are retrieved from there, otherwise the default
flags are used.
Note: This function is only called by ycmd if no flags have been loaded
for the file. Once flags are loaded it is not called again. Or at least
that appears to be the behavior.
Important Note: If precompiled headers are not working then the libclang
used by ycmd is different than the system libclang. The solution seems
to be setting ycmd to use the system libclang.
"""
def basic_flags():
"""Returns the default flags if no database is found"""
return {'flags': list(default_flags),
'include_paths_relative_to_dir': os.path.split(filename)[0]}
# Check if there is a database candidate for this file
dir_and_db = next((db for db in completion_database if db[0] in filename),
None)
# If no database candidate is found, try to load a database,
# otherwise return the basic flags
if dir_and_db is None:
db_candidate = FindCompilationDatabaseAndDir(
os.path.split(filename)[0])
if db_candidate is None:
return basic_flags()
completion_database.append(db_candidate)
dir_and_db = db_candidate
# Check if file can be found in compilation database, if not, try
# refreshing the database. Refreshing is used to handle files being
# added to a project.
compilation_info = GetCompilationInfoForFile(filename,
dir_and_db[0],
dir_and_db[1])
# Refresh compilation database
if compilation_info is None:
dir_and_db = ReloadCompilationDatabase(dir_and_db[0])
compilation_info = GetCompilationInfoForFile(
filename, dir_and_db[0], dir_and_db[1])
if compilation_info is None:
# We could not find flags for the file even though it is in a
# sub-directory of the directory that the database is in. This is
# taken as meaning that the user has not yet added the file to the
# project's build system and we should try loading flags again
# later. By return "None" we signal to YCMD that it should later
# try loading the flags again.
return None
return {'flags': list(compilation_info.compiler_flags_),
'include_paths_relative_to_dir':
compilation_info.compiler_working_dir_}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment