Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide nicer error messages on misuse of the library #8

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ BraceWrapping:
SplitEmptyNamespace: true
ColumnLimit: 100
IndentWidth: 4
IndentPPDirectives: AfterHash
Cpp11BracedListStyle: true
NamespaceIndentation: All
Standard: Cpp11
Expand Down
28 changes: 12 additions & 16 deletions cmake/TestCompileError.cmake
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
find_package(Python3 COMPONENTS Interpreter REQUIRED)

set(__TEST_COMPILE_ERROR_PYTHON_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/test-compile-error.py)

function(add_compile_failure_test file)
get_filename_component(abspath ${file} ABSOLUTE)
file(RELATIVE_PATH target_name ${CMAKE_SOURCE_DIR} ${abspath})
string(MAKE_C_IDENTIFIER ${target_name} target_name)
set(target_name compilefailure.test.${target_name})

cmake_parse_arguments(PARSE_ARGV 1 ARG "" "" "LINK_LIBRARIES")
cmake_parse_arguments(PARSE_ARGV 1 ARG "" "" "LINK_LIBRARIES;COMPILE_OPTIONS")

if(ARG_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "Unknown arguments ${ARG_UNPARSED_ARGUMENTS}")
endif()

add_library(${target_name} OBJECT ${file})
target_link_libraries(${target_name} PRIVATE ${ARG_LINK_LIBRARIES})
target_compile_options(${target_name} PRIVATE ${ARG_COMPILE_OPTIONS})
set_property(TARGET ${target_name} PROPERTY EXCLUDE_FROM_ALL TRUE)
set_property(TARGET ${target_name} PROPERTY EXCLUDE_FROM_DEFAULT_BUILD TRUE)
set_property(SOURCE ${file} PROPERTY COMPILE_FAILURE_TEST_TARGET ${target_name})
Expand All @@ -23,20 +28,11 @@ function(add_compile_failure_test file)
)
add_test(
NAME test.${target_name}
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --config $<CONFIG> --target testrunner.${target_name}
)
set_tests_properties(test.${target_name}
PROPERTIES
WILL_FAIL TRUE
COMMAND Python3::Interpreter ${__TEST_COMPILE_ERROR_PYTHON_SCRIPT}
--cmake ${CMAKE_COMMAND}
--config $<CONFIG>
--binary_dir ${CMAKE_BINARY_DIR}
--target testrunner.${target_name}
--file ${abspath}
)
endfunction()

function(add_compile_failure_tests)
list(FIND ARGV LINK_LIBRARIES link_libraries_index)
list(SUBLIST ARGV 0 ${link_libraries_index} files)
list(SUBLIST ARGV ${link_libraries_index} -1 link_libraries)

foreach(file IN LISTS files)
add_compile_failure_test(${file} ${link_libraries})
endforeach()
endfunction()
76 changes: 76 additions & 0 deletions cmake/test-compile-error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import argparse
import subprocess
import sys
import re


if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Builds a CMake target that might fail')
parser.add_argument('--cmake', required=True, help='The ${CMAKE_COMMAND}')
parser.add_argument('--config', required=True,
help='The build configuration')
parser.add_argument('--binary_dir', required=True,
help='The ${CMAKE_BINARY_DIR}')
parser.add_argument('--target', required=True,
help='The CMake target to build')
parser.add_argument('--file', required=True, help='The source file')

args = parser.parse_args()

cmd = [args.cmake, '--build', args.binary_dir,
'--config', args.config, '--target', args.target]
print(' '.join(f"'{part}'" for part in cmd), flush=True)
result = subprocess.run(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, encoding='utf-8')
result.stderr = result.stdout

with open(args.file, 'r') as f:
contents = f.read()

error_matches = re.finditer(
r'// ERROR_MATCHES: (.*)$', contents, re.MULTILINE)
error_not_matches = re.finditer(
r'// ERROR_NOT_MATCHES: (.*)$', contents, re.MULTILINE)

for re_match in error_matches:
regex = re_match.group(1)
try:
m = re.search(regex, result.stderr)
except:
print(
f'Regular Expression: {repr(regex)}', file=sys.stderr)
raise
if not m:
print(
f'Output did not match when it should have.\n'
+ f'Regular Expression: {regex}\n'
+ 'Stderr:\n'
+ result.stderr, file=sys.stderr)
exit(1)

for re_match in error_not_matches:
regex = re_match.group(1)
try:
m = re.search(regex, result.stderr)
except:
print(
f'Regular Expression: {repr(regex)}', file=sys.stderr)
raise
if m:
print(
f'Output matched when it should NOT have.\n'
+ f'Regular Expression: {regex}\n'
+ f'Match: {m.group()}\n'
+ 'Stderr:\n'
+ result.stderr, file=sys.stderr)
exit(1)

if not error_matches and not error_not_matches:
if result.returncode == 0:
print(f'Expected compile failure, but compile succeeded.', file=sys.stderr)
exit(1)

print('Test Passed.\n'
+ 'Stderr\n'
+ result.stderr)
84 changes: 75 additions & 9 deletions include/nickel/nickel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,65 @@
#include <tuple>
#include <type_traits>

#if defined(__clang__)
# define NICKEL_DETAIL_IS_CLANG
#elif defined(__GNUC__)
# define NICKEL_DETAIL_IS_GCC
#elif defined(_MSC_VER)
# define NICKEL_DETAIL_IS_MSVC
#endif

#define NICKEL_DETAIL_STR2(...) #__VA_ARGS__
#define NICKEL_DETAIL_STR(...) NICKEL_DETAIL_STR2(__VA_ARGS__)

#ifdef NICKEL_DETAIL_IS_CLANG
# define NICKEL_DETAIL_CLANG_PRAGMA(...) _Pragma(NICKEL_DETAIL_STR(__VA_ARGS__))
#else
# define NICKEL_DETAIL_CLANG_PRAGMA(...)
#endif
#define NICKEL_DETAIL_CLANG_WIGNORE(warning, ...) \
NICKEL_DETAIL_CLANG_PRAGMA(clang diagnostic push) \
NICKEL_DETAIL_CLANG_PRAGMA(clang diagnostic ignored warning) \
__VA_ARGS__ NICKEL_DETAIL_CLANG_PRAGMA(clang diagnostic pop)

#ifdef NICKEL_DETAIL_IS_GCC
# define NICKEL_DETAIL_GCC_PRAGMA(...) _Pragma(NICKEL_DETAIL_STR(__VA_ARGS__))
#else
# define NICKEL_DETAIL_GCC_PRAGMA(...)
#endif
#define NICKEL_DETAIL_GCC_WIGNORE(warning, ...) \
NICKEL_DETAIL_GCC_PRAGMA(GCC diagnostic push) \
NICKEL_DETAIL_GCC_PRAGMA(GCC diagnostic ignored warning) \
__VA_ARGS__ NICKEL_DETAIL_GCC_PRAGMA(GCC diagnostic pop)

#ifdef NICKEL_DETAIL_IS_MSVC
# define NICKEL_DETAIL_MSVC_PRAGMA(...) _Pragma(NICKEL_DETAIL_STR(__VA_ARGS__))
#else
# define NICKEL_DETAIL_MSVC_PRAGMA(...)
#endif

#define NICKEL_CLANG_PRAGMA NICKEL_DETAIL_CLANG_PRAGMA
#define NICKEL_CLANG_WIGNORE NICKEL_DETAIL_CLANG_WIGNORE
#define NICKEL_GCC_PRAGMA NICKEL_DETAIL_GCC_PRAGMA
#define NICKEL_GCC_WIGNORE NICKEL_DETAIL_GCC_WIGNORE
#define NICKEL_MSVC_PRAGMA NICKEL_DETAIL_MSVC_PRAGMA

#ifdef __has_cpp_attribute
# if __has_cpp_attribute(nodiscard) >= 201907
# define NICKEL_DETAIL_NODISCARD(...) \
NICKEL_CLANG_WIGNORE("-Wc++20-extensions", [[nodiscard(__VA_ARGS__)]])
# elif __has_cpp_attribute(nodiscard)
# define NICKEL_DETAIL_NODISCARD(...) \
NICKEL_CLANG_WIGNORE("-Wc++17-extensions", [[nodiscard]])
# else
# define NICKEL_DETAIL_NODISCARD(...)
# endif
#else
# define NICKEL_DETAIL_NODISCARD(...)
#endif

#define NICKEL_NODISCARD NICKEL_DETAIL_NODISCARD

// std::forward
#define NICKEL_DETAIL_FWD(...) static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)
// std::move
Expand All @@ -27,13 +86,13 @@

// Wraps std::trait_v<...> to work pre-C++17.
#ifdef __cpp_lib_type_trait_variable_templates
#define NICKEL_IS_VOID(...) std::is_void_v<__VA_ARGS__>
#define NICKEL_IS_SAME(...) std::is_same_v<__VA_ARGS__>
#define NICKEL_IS_RVALUE_REFERENCE(...) std::is_rvalue_reference_v<__VA_ARGS__>
# define NICKEL_IS_VOID(...) std::is_void_v<__VA_ARGS__>
# define NICKEL_IS_SAME(...) std::is_same_v<__VA_ARGS__>
# define NICKEL_IS_RVALUE_REFERENCE(...) std::is_rvalue_reference_v<__VA_ARGS__>
#else
#define NICKEL_IS_VOID(...) std::is_void<__VA_ARGS__>::value
#define NICKEL_IS_SAME(...) std::is_same<__VA_ARGS__>::value
#define NICKEL_IS_RVALUE_REFERENCE(...) std::is_rvalue_reference<__VA_ARGS__>::value
# define NICKEL_IS_VOID(...) std::is_void<__VA_ARGS__>::value
# define NICKEL_IS_SAME(...) std::is_same<__VA_ARGS__>::value
# define NICKEL_IS_RVALUE_REFERENCE(...) std::is_rvalue_reference<__VA_ARGS__>::value
#endif

namespace nickel {
Expand Down Expand Up @@ -430,9 +489,10 @@ namespace nickel {
typename Kwargs, // Any Kwargs
typename Names, // The explicit named parameters
typename CallEvalPolicy> // How to implement calling `Fn`.
class wrapped_fn : public wrapped_fn_base<
wrapped_fn<Defaults, Storage, Fn, Kwargs, Names, CallEvalPolicy>,
Storage, Kwargs, Names>
class NICKEL_NODISCARD("Should be invoked with a bare () at the end") wrapped_fn
: public wrapped_fn_base<
wrapped_fn<Defaults, Storage, Fn, Kwargs, Names, CallEvalPolicy>, Storage, Kwargs,
Names>
{
private:
Defaults defaults_;
Expand Down Expand Up @@ -866,5 +926,11 @@ namespace nickel {
#undef NICKEL_IS_VOID
#undef NICKEL_IS_SAME
#undef NICKEL_IS_RVALUE_REFERENCE
#undef NICKEL_NODISCARD
#undef NICKEL_MSVC_PRAGMA
#undef NICKEL_GCC_WIGNORE
#undef NICKEL_GCC_PRAGMA
#undef NICKEL_CLANG_WIGNORE
#undef NICKEL_CLANG_PRAGMA

#endif
22 changes: 18 additions & 4 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,23 @@ catch_discover_tests(test.nickel

# Test that compilation fails
include(TestCompileError)
file(GLOB_RECURSE compile_failure_test_sources CONFIGURE_DEPENDS "compile_error/*.test.cpp")
include(CheckCXXSourceCompiles)

add_compile_failure_tests(
${compile_failure_test_sources}
LINK_LIBRARIES nickel::nickel
check_cxx_source_compiles([==[
struct [[nodiscard]] SomeType {};

SomeType someFunction() { return {}; }

int main() {
auto x = someFunction();
}
]==]
NODISCARD_SUPPORTED
)

link_libraries(nickel::nickel)
add_compile_failure_test(compile_error/steal.nonrvalue.test.cpp)

if(NODISCARD_SUPPORTED)
add_compile_failure_test(compile_error/missing_eval_void.test.cpp)
endif()
20 changes: 20 additions & 0 deletions tests/compile_error/missing_eval_void.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright Justin Bassett 2019 - 2021.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)

#include <nickel/nickel.hpp>

// ERROR_MATCHES: missing_eval_void.test.cpp\D*19\b

NICKEL_NAME(abc);

auto do_something()
{
return nickel::wrap(abc)([](int abc) { (void)abc; });
}

void test()
{
do_something().abc(2);
}