From 8746aa57aec43cf8670789a33d98a8bd49bf4cfe Mon Sep 17 00:00:00 2001 From: Michael Fabian 'Xaymar' Dirks Date: Sat, 18 Apr 2020 16:17:15 +0200 Subject: [PATCH] clang-tidy: Compile Command Database generation and clang-tidy Adds support for clang-tidy by the use of a manually generated compile command database. This is likely not full functional yet, so more tests might have to be done on different platforms and compilers. --- Clang.cmake | 379 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 352 insertions(+), 27 deletions(-) diff --git a/Clang.cmake b/Clang.cmake index d4344cf..b724eb1 100644 --- a/Clang.cmake +++ b/Clang.cmake @@ -1,9 +1,224 @@ set(CLANG_PATH "" CACHE PATH "Path to Clang Toolset (if not in environment)") +function(string_escape) + cmake_parse_arguments( + PARSE_ARGV 0 + _ARGS + "" + "OUTPUT;INPUT" + "" + ) + + set(_el "${_ARGS_INPUT}") + string(REPLACE "\\" "\\\\" _el "${_el}") + string(REPLACE "\"" "\\\"" _el "${_el}") + set(${_ARGS_OUTPUT} "${_el}" PARENT_SCOPE) +endfunction() + +function(string_append_escaped) + cmake_parse_arguments( + PARSE_ARGV 0 + _ARGS + "" + "OUTPUT;INPUT" + "" + ) + + set(_el "${_ARGS_INPUT}") + if(_el) + string_escape(OUTPUT _el INPUT "${_el}") + set(${_ARGS_OUTPUT} "${${_ARGS_OUTPUT}}${_el}" PARENT_SCOPE) + endif() +endfunction() + +function(string_append_target_includes) + cmake_parse_arguments( + PARSE_ARGV 0 + _ARGS + "LINKED" + "OUTPUT;PREFIX;TARGET" + "" + ) + + set(_out "") + if(TARGET ${_ARGS_TARGET}) + get_target_property(target_type ${_ARGS_TARGET} TYPE) + if( + (${target_type} STREQUAL "MODULE_LIBRARY") OR + (${target_type} STREQUAL "STATIC_LIBRARY") OR + (${target_type} STREQUAL "SHARED_LIBRARY") OR + (${target_type} STREQUAL "EXECUTABLE") + ) + set(prop "$") + set(test "$<$:${_ARGS_PREFIX}$>") + set(_out "${_out}${test} ") + set(prop "$") + set(test "$<$:${_ARGS_PREFIX}$>") + set(_out "${_out}${test} ") + set(prop "$") + set(test "$<$:${_ARGS_PREFIX}$>") + set(_out "${_out}${test} ") + if(NOT _ARGS_LINKED) + # Scan linked libraries as well. + get_target_property(_els ${_target} LINK_LIBRARIES) + foreach(_lib ${_els}) + #string_append_target_includes(LINKED TARGET "${_lib}" OUTPUT "_out" PREFIX "${_ARGS_PREFIX}") + endforeach() + endif() + elseif((${target_type} STREQUAL "INTERFACE_LIBRARY")) + set(prop "$") + set(test "$<$:${_ARGS_PREFIX}$>") + set(_out "${_out}${test} ") + else() + message("clang: Unsupported Target type '${target_type}', please open an issue for this.") + endif() + endif() + + set(${_ARGS_OUTPUT} "${${_ARGS_OUTPUT}}${_out}" PARENT_SCOPE) +endfunction() + +function(generate_compile_commands_json) + cmake_parse_arguments( + PARSE_ARGV 0 + _ARGS + "" + "REGEX" + "TARGETS" + ) + + set(COMPILER_TAG "") + if((CMAKE_C_COMPILER_ID STREQUAL "MSVC") AND (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")) + set(COMPILER_TAG "MSVC") + set(COMPILER_INCLUDE_PREFIX "/I") + set(COMPILER_DEFINE_PREFIX "/D") + elseif((CMAKE_C_COMPILER_ID STREQUAL "GNU") AND (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")) + set(COMPILER_TAG "GNU") + set(COMPILER_INCLUDE_PREFIX "-I") + set(COMPILER_DEFINE_PREFIX "-D") + elseif((CMAKE_C_COMPILER_ID STREQUAL "Clang") AND (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) + set(COMPILER_TAG "CLANG") # Compatible with GNU + set(COMPILER_INCLUDE_PREFIX "-I") + set(COMPILER_DEFINE_PREFIX "-D") + else() + message("clang-tidy: C ID '${CMAKE_C_COMPILER_ID}'") + message("clang-tidy: C Version '${CMAKE_C_COMPILER_VERSION}'") + message("clang-tidy: C++ ID '${CMAKE_CXX_COMPILER_ID}'") + message("clang-tidy: C++ Version '${CMAKE_CXX_COMPILER_VERSION}'") + message(FATAL_ERROR "clang-tidy: Current Compiler is not yet supported, please open an issue for it.") + endif() + + # Default Filter + if(NOT _ARGS_REGEX) + set(_ARGS_REGEX "\.(h|hpp|c|cpp)$") + endif() + + foreach(_target ${_ARGS_TARGETS}) + set(COMPILE_COMMAND_JSON "") + + # Source Directory + get_target_property(target_source_dir_rel ${_target} SOURCE_DIR) + get_filename_component(target_source_dir ${target_source_dir_rel} ABSOLUTE) + unset(target_source_dir_rel) + + # Binary Directory + get_target_property(target_binary_dir_rel ${_target} BINARY_DIR) + get_filename_component(target_binary_dir ${target_binary_dir_rel} ABSOLUTE) + unset(target_binary_dir_rel) + + # Sources + get_target_property(_els ${_target} SOURCES) + set(target_sources "") + foreach(_el ${_els}) + get_filename_component(_el "${_el}" ABSOLUTE) + list(APPEND target_sources "${_el}") + endforeach() + list(FILTER target_sources INCLUDE REGEX "${_ARGS_REGEX}") + + # Combine Compiler String + set(COMPILER_OPTIONS "") + ## Compiler Options + get_target_property(_els ${_target} COMPILE_OPTIONS) + foreach(_el ${_els}) + string_append_escaped(OUTPUT COMPILER_OPTIONS INPUT "${_el} ") + endforeach() + string(APPEND COMPILER_OPTIONS "${CMAKE_CXX_FLAGS} ${CMAKE_C_FLAGS} ") + ## C++ Standard + get_target_property(_el ${_target} CXX_STANDARD) + if(COMPILER_TAG STREQUAL "MSVC") + if(${MSVC_VERSION} LESS "1920") + message(FATAL_ERROR "clang-tidy: Current Compiler is not yet supported, please open an issue for it.") + else() + if((_el EQUAL 98) OR (_el EQUAL 11)) + # Nothing to do, this is the default. + elseif(_el EQUAL 14) + string(APPEND COMPILER_OPTIONS "/std:c++14 ") + elseif(_el EQUAL 17) + string(APPEND COMPILER_OPTIONS "/std:c++17 ") + elseif(_el EQUAL 20) + string(APPEND COMPILER_OPTIONS "/std:c++latest ") + endif() + endif() + elseif((COMPILER_TAG STREQUAL "CLANG") OR (COMPILER_TAG STREQUAL "GNU")) + if(_el EQUAL 98) + string(APPEND COMPILER_OPTIONS "-std=c++98 ") + elseif(_el EQUAL 11) + string(APPEND COMPILER_OPTIONS "-std=c++11 ") + elseif(_el EQUAL 14) + string(APPEND COMPILER_OPTIONS "-std=c++14 ") + elseif(_el EQUAL 17) + string(APPEND COMPILER_OPTIONS "-std=c++17 ") + elseif(_el EQUAL 20) + if((COMPILER_TAG STREQUAL "CLANG") OR (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9)) + string(APPEND COMPILER_OPTIONS "-std=c++20 ") + else() + string(APPEND COMPILER_OPTIONS "-std=c++2a ") + endif() + endif() + endif() + ## Definitions + get_target_property(_els ${_target} COMPILE_DEFINITIONS) + foreach(_el ${_els}) + string_append_escaped(OUTPUT COMPILER_OPTIONS INPUT "${COMPILER_DEFINE_PREFIX}${_el} ") + endforeach() + ## Includes + string_append_target_includes(TARGET ${_target} PREFIX ${COMPILER_INCLUDE_PREFIX} OUTPUT COMPILER_OPTIONS) + + # Create Compilation Database + set(target_compile_db "${target_binary_dir}/compile_commands.json") + string(APPEND COMPILE_COMMAND_JSON "[\n") + foreach(_el ${target_sources}) + file(TO_NATIVE_PATH "${_el}" _el) + string(REPLACE "\\" "\\\\" _el "${_el}") + string(REPLACE "\"" "\\\"" _el "${_el}") + + string(APPEND COMPILE_COMMAND_JSON "\t{\n") + string(APPEND COMPILE_COMMAND_JSON "\t\t\"directory\": \"${target_binary_dir}\",\n") + string(APPEND COMPILE_COMMAND_JSON "\t\t\"file\": \"${_el}\",\n") + if(MSVC) + string(APPEND COMPILE_COMMAND_JSON "\t\t\"command\": \"cl ") + else() + string(APPEND COMPILE_COMMAND_JSON "\t\t\"command\": \"cc ") + endif() + + string(APPEND COMPILE_COMMAND_JSON "${COMPILER_OPTIONS}") + + if(MSVC) + string(APPEND COMPILE_COMMAND_JSON "${_el}") + else() + string(APPEND COMPILE_COMMAND_JSON " -c ${_el}") + endif() + string(APPEND COMPILE_COMMAND_JSON "\",\n\t},\n") + endforeach() + string(APPEND COMPILE_COMMAND_JSON "]") + + file(GENERATE OUTPUT "$/$/compile_commands.json" CONTENT "${COMPILE_COMMAND_JSON}") + endforeach() +endfunction() + function(clang_format) cmake_parse_arguments( PARSE_ARGV 0 - _CLANG_FORMAT + _ARGS "DEPENDENCY;GLOBAL" "REGEX;VERSION" "TARGETS" @@ -13,7 +228,7 @@ function(clang_format) "clang-format" DOC "Path (or name) of the clang-format binary" HINTS - ${CLANG_PATH} + "${CLANG_PATH}" PATHS /bin /sbin @@ -30,44 +245,43 @@ function(clang_format) endif() # Validate Version - if (_CLANG_FORMAT_VERSION) + if (_ARGS_VERSION) set(_VERSION_RESULT "") set(_VERSION_OUTPUT "") execute_process( COMMAND "${CLANG_FORMAT_BIN}" --version - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" RESULT_VARIABLE _VERSION_RESULT OUTPUT_VARIABLE _VERSION_OUTPUT - OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE ERROR_QUIET - OUTPUT_QUIET ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE + ERROR_QUIET ) if(NOT _VERSION_RESULT EQUAL 0) message(WARNING "Clang: Could not discover version, disabling clang-format...") return() endif() string(REGEX MATCH "([0-9]+\.[0-9]+\.[0-9]+)" _VERSION_MATCH ${_VERSION_OUTPUT}) - if(NOT ${_VERSION_MATCH} VERSION_GREATER_EQUAL ${_CLANG_FORMAT_VERSION}) + if(NOT ${_VERSION_MATCH} VERSION_GREATER_EQUAL ${_ARGS_VERSION}) message(WARNING "Clang: Old version discovered, disabling clang-format...") return() endif() endif() # Default Filter - if(NOT _CLANG_FORMAT_FILTER) - set(_CLANG_FORMAT_FILTER "\.(h|hpp|c|cpp)$") + if(NOT _ARGS_REGEX) + set(_ARGS_REGEX "\.(h|hpp|c|cpp)$") endif() # Go through each target - foreach(_target ${_CLANG_FORMAT_TARGETS}) -# get_target_property(target_name ${_target} NAME) - + foreach(_target ${_ARGS_TARGETS}) get_target_property(target_sources_rel ${_target} SOURCES) set(target_sources "") - foreach(source_relative ${target_sources_rel}) - get_filename_component(source_absolute ${source_relative} ABSOLUTE) - list(APPEND target_sources ${source_absolute}) + foreach(_el ${target_sources_rel}) + get_filename_component(_el "${_el}" ABSOLUTE) + file(TO_NATIVE_PATH "${_el}" _el) + list(APPEND target_sources "${_el}") endforeach() - list(FILTER target_sources INCLUDE REGEX "${_CLANG_FORMAT_FILTER}") + list(FILTER target_sources INCLUDE REGEX "${_ARGS_REGEX}") unset(target_sources_rel) get_target_property(target_source_dir_rel ${_target} SOURCE_DIR) @@ -75,22 +289,16 @@ function(clang_format) unset(target_source_dir_rel) add_custom_target(${_target}_CLANG-FORMAT - COMMAND - ${CLANG_FORMAT_BIN} - -style=file - -i - ${target_sources} - COMMENT - "clang-format: Formatting ${_target}..." - WORKING_DIRECTORY - ${target_source_dir_rel} + COMMAND "${CLANG_FORMAT_BIN}" -style=file -i ${target_sources} + COMMENT "clang-format: Formatting ${_target}..." + WORKING_DIRECTORY "${target_source_dir}" ) - if(_CLANG_FORMAT_DEPENDENCY) + if(_ARGS_DEPENDENCY) add_dependencies(${_target} ${_target}_CLANG-FORMAT) endif() - if(_CLANG_FORMAT_GLOBAL) + if(_ARGS_GLOBAL) if(TARGET CLANG-FORMAT) add_dependencies(CLANG-FORMAT ${_target}_CLANG-FORMAT) else() @@ -104,3 +312,120 @@ function(clang_format) endif() endforeach() endfunction() + +function(clang_tidy) + cmake_parse_arguments( + PARSE_ARGV 0 + _ARGS + "DEPENDENCY;GLOBAL" + "REGEX;VERSION" + "TARGETS" + ) + + find_program(CLANG_TIDY_BIN + "clang-tidy" + DOC "Path (or name) of the clang-tidy binary" + HINTS + "${CLANG_PATH}" + PATHS + /bin + /sbin + /usr/bin + /usr/local/bin + PATH_SUFFIXES + bin + bin64 + bin32 + ) + if(NOT CLANG_TIDY_BIN) + message(WARNING "Clang: Could not find clang-tidy at path '${CLANG_TIDY_BIN}', disabling clang-tidy...") + return() + endif() + + # Validate Version + if (_ARGS_VERSION) + set(_VERSION_RESULT "") + set(_VERSION_OUTPUT "") + execute_process( + COMMAND "${CLANG_TIDY_BIN}" --version + WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" + RESULT_VARIABLE _VERSION_RESULT + OUTPUT_VARIABLE _VERSION_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + if(NOT _VERSION_RESULT EQUAL 0) + message(WARNING "Clang: Could not discover version, disabling clang-tidy...") + return() + endif() + string(REGEX MATCH "([0-9]+\.[0-9]+\.[0-9]+)" _VERSION_MATCH ${_VERSION_OUTPUT}) + if(NOT ${_VERSION_MATCH} VERSION_GREATER_EQUAL ${_ARGS_VERSION}) + message(WARNING "Clang: Old version discovered, disabling clang-tidy...") + return() + endif() + endif() + + # Default Filter + if(NOT _ARGS_REGEX) + set(_ARGS_REGEX "\.(h|hpp|c|cpp)$") + endif() + + # Go through each target + foreach(_target ${_ARGS_TARGETS}) + # Source Directory + get_target_property(_els ${_target} SOURCE_DIR) + get_filename_component(target_source_dir ${_els} ABSOLUTE) + file(TO_NATIVE_PATH "${target_source_dir}" target_source_dir_nat) + unset(_els) + + # Binary Directory + get_target_property(_els ${_target} BINARY_DIR) + get_filename_component(target_binary_dir ${_els} ABSOLUTE) + file(TO_NATIVE_PATH "${target_binary_dir}" target_binary_dir_nat) + unset(_els) + + # Sources + get_target_property(_els ${_target} SOURCES) + set(target_sources "") + foreach(_el ${_els}) + get_filename_component(_el ${_el} ABSOLUTE) + file(TO_NATIVE_PATH "${_el}" _el) + list(APPEND target_sources "${_el}") + endforeach() + list(FILTER target_sources INCLUDE REGEX "${_ARGS_REGEX}") + unset(_els) + + add_custom_target(${_target}_CLANG-TIDY + COMMENT "clang-tiy: Tidying ${_target}..." + WORKING_DIRECTORY "${target_binary_dir}" + VERBATIM + ) + foreach(_el ${target_sources}) + add_custom_command( + TARGET ${_target}_CLANG-TIDY + POST_BUILD + COMMAND "${CLANG_TIDY_BIN}" + ARGS --quiet -p="$/$" "${_el}" + WORKING_DIRECTORY "${target_binary_dir}" + COMMAND_EXPAND_LISTS + ) + endforeach() + + if(_ARGS_DEPENDENCY) + add_dependencies(${_target} ${_target}_CLANG-TIDY) + endif() + + if(_ARGS_GLOBAL) + if(TARGET CLANG-TIDY) + add_dependencies(CLANG-TIDY ${_target}_CLANG-FORTIDYMAT) + else() + add_custom_target(CLANG-TIDY + DEPENDS + ${_target}_CLANG-TIDY + COMMENT + "clang-tiy: Tidying..." + ) + endif() + endif() + endforeach() +endfunction()