diff --git a/CMakeLists.txt b/CMakeLists.txt index dbf2104..c813f8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,13 +119,13 @@ add_library(libchimera ) target_compile_options(libchimera PUBLIC "-std=c++11") target_link_libraries(libchimera - PRIVATE + PUBLIC ${YAMLCPP_LIBRARIES} ${CLANG_LIBS} ${llvm_libs} ) -target_link_libraries(libchimera PRIVATE mstch cling_utils) -target_link_libraries(libchimera PRIVATE chimera_bindings) +target_link_libraries(libchimera PUBLIC mstch cling_utils) +target_link_libraries(libchimera PUBLIC chimera_bindings) target_compile_definitions(libchimera PUBLIC CHIMERA_MAJOR_VERSION=${CHIMERA_MAJOR_VERSION} diff --git a/include/chimera/configuration.h b/include/chimera/configuration.h index d8d37bd..c0e0f8d 100644 --- a/include/chimera/configuration.h +++ b/include/chimera/configuration.h @@ -120,15 +120,37 @@ class Configuration class CompiledConfiguration { public: + enum class StaticMethodNamePolicy + { + NO_CHANGE, + // Cast all the letters to upper case + TO_UPPER, + // Cast all the letters to lower case + TO_LOWER, + // Cast the first letter to upper case and cast all the letters followed + // by underscore to upper cases removing the underscores + TO_PASCAL, + // Cast the first letter to lower case and cast all the letters followed + // by underscore to upper cases removing the underscores + TO_CAMEL, + // TODO: Add more policies such as prefix and suffix + }; + virtual ~CompiledConfiguration() = default; CompiledConfiguration(const CompiledConfiguration &) = delete; CompiledConfiguration &operator=(const CompiledConfiguration &) = delete; /** - * Return whether to treat unresolvable configuration as errors. + * Returns whether to treat unresolvable configuration as errors. */ bool GetStrict() const; + /** + * Returns policy for the case that a static method has a same name one of + * the instance method names in the same class. + */ + StaticMethodNamePolicy GetStaticMethodNamePolicy() const; + /** * Adds a namespace to an ordered set of traversed namespaces. * This set can later be rendered in a template. @@ -258,6 +280,7 @@ class CompiledConfiguration std::set binding_namespace_decls_; bool strict_; + StaticMethodNamePolicy static_method_name_policy_; friend class Configuration; }; diff --git a/include/chimera/mstch.h b/include/chimera/mstch.h index 7d85519..a966b9a 100644 --- a/include/chimera/mstch.h +++ b/include/chimera/mstch.h @@ -281,7 +281,7 @@ class Function : public ClangWrapper, const int argument_limit = -1); ::mstch::node type(); - ::mstch::node overloads(); + virtual ::mstch::node overloads(); ::mstch::node params(); ::mstch::node returnType(); ::mstch::node returnValuePolicy(); @@ -295,8 +295,10 @@ class Function : public ClangWrapper, ::mstch::node call(); ::mstch::node qualifiedCall(); -private: +protected: const clang::CXXRecordDecl *class_decl_; + +private: const int argument_limit_; }; @@ -305,15 +307,23 @@ class Method : public Function public: Method(const ::chimera::CompiledConfiguration &config, const clang::CXXMethodDecl *decl, - const clang::CXXRecordDecl *class_decl = nullptr); + const clang::CXXRecordDecl *class_decl = nullptr, + const int argument_limit = -1); + + void setNameConflict(bool val); + bool isNameConflict() const; + ::mstch::node overloads() override; + std::string nameAsString() override; ::mstch::node isConst(); + bool isStaticAsBool() const; ::mstch::node isStatic(); ::mstch::node isVirtual(); ::mstch::node isPureVirtual(); private: const clang::CXXMethodDecl *method_decl_; + bool name_conflict_; }; class Namespace : public ClangWrapper diff --git a/include/chimera/util.h b/include/chimera/util.h index afaa652..bef956a 100644 --- a/include/chimera/util.h +++ b/include/chimera/util.h @@ -326,6 +326,38 @@ std::string trimLeft(std::string s, const char *t = " \t\n\r\f\v"); */ std::string trim(std::string s, const char *t = " \t\n\r\f\v"); +/** + * Casts all the letters to upper case + */ +std::string toUpper(const std::string &str, int n = -1); + +/** + * Casts all the letters to lower case + */ +std::string toLower(const std::string &str, int n = -1); + +/** + * Casts the first letter to upper case and casts all the letters followed by + * underscore to upper cases removing the underscores + */ +std::string toPascal(const std::string &str); + +/** + * Casts the first letter to lower case and casts all the letters followed by + * underscore to upper cases removing the underscores + */ +std::string toCamel(const std::string &str); + +/** + * Splits string into a list of tokens. + * + * @param[in] str The original string to be split. + * @param[in] delimiter The string delimiter, which can be more than one + * character. + */ +std::vector split(const std::string &str, + const std::string &delimiter = " "); + /** * Returns true if a string starts with a prefix, otherwise false. */ diff --git a/src/configuration.cpp b/src/configuration.cpp index b176163..7c438e6 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -193,6 +193,51 @@ chimera::CompiledConfiguration::CompiledConfiguration( config_options_strict = strictNode.as(); } + // Parse 'options.static_method_name_policy' to set the static method name + // policy for the case a static method has the same name one of the instance + // methods in the same class. + static_method_name_policy_ = StaticMethodNamePolicy::NO_CHANGE; + const YAML::Node staticMethodNamePolicyNode = chimera::util::lookupYAMLNode( + configNode_, "options", "static_method_name_policy"); + if (staticMethodNamePolicyNode) + { + const std::string policy = staticMethodNamePolicyNode.as(); + if (policy == "to_upper") + { + static_method_name_policy_ = StaticMethodNamePolicy::TO_UPPER; + } + else if (policy == "to_lower") + { + static_method_name_policy_ = StaticMethodNamePolicy::TO_UPPER; + } + else if (policy == "to_pascal") + { + static_method_name_policy_ = StaticMethodNamePolicy::TO_PASCAL; + } + else if (policy == "to_camel") + { + static_method_name_policy_ = StaticMethodNamePolicy::TO_CAMEL; + } + else + { + if (GetStrict()) + { + throw std::runtime_error( + "Invalid value for 'static_method_name_policy': '" + policy + + "'. Choose one of following: 'to_upper', 'to_lower', 'to_camel', 'to_pascal'"); + } + else + { + std::cerr + << "Invalid value for 'static_method_name_policy': '" + << policy + << "'. Choose one of following: 'no_change', 'to_upper', " + << "'to_lower', 'to_camel', 'to_pascal'. Defaulting to " + << "'no_change'." << std::endl; + } + } + } + // Set the options.strict from one of the following sources in order of // priority: 1) CLI '-strict' setting (True if -strict passed, False // otherwise) 2) YAML configuration setting (True/False) 3) false by default @@ -268,7 +313,7 @@ chimera::CompiledConfiguration::CompiledConfiguration( } else { - std::cout << "Warning: Skipped namespace namespace '" + std::cerr << "Warning: Skipped namespace namespace '" << ns_str << "' because it's unable to resolve " << "the namespace." << std::endl; @@ -324,7 +369,7 @@ chimera::CompiledConfiguration::CompiledConfiguration( } else { - std::cout + std::cerr << "Warning: Skipped the configuration for class '" << decl_str << "' becuase it's " << "unable to resolve the class declaration." @@ -363,7 +408,7 @@ chimera::CompiledConfiguration::CompiledConfiguration( } else { - std::cout + std::cerr << "Warning: Skipped the configuration for " << "function '" << decl_str << "' becuase it's " << "unable to resolve the function declaration." @@ -402,7 +447,7 @@ chimera::CompiledConfiguration::CompiledConfiguration( } else { - std::cout << "Warning: Skipped the configuration for " + std::cerr << "Warning: Skipped the configuration for " << "type '" << type_str << "' becuase it's " << "unable to resolve the type." << std::endl; continue; @@ -478,6 +523,12 @@ bool chimera::CompiledConfiguration::GetStrict() const return strict_; } +chimera::CompiledConfiguration::StaticMethodNamePolicy +chimera::CompiledConfiguration::GetStaticMethodNamePolicy() const +{ + return static_method_name_policy_; +} + void chimera::CompiledConfiguration::AddTraversedNamespace( const clang::NamespaceDecl *decl) { diff --git a/src/mstch.cpp b/src/mstch.cpp index f1d2c1d..ed96486 100644 --- a/src/mstch.cpp +++ b/src/mstch.cpp @@ -3,8 +3,10 @@ #include "chimera/util.h" #include "cling_utils_AST.h" +#include #include #include +#include #include #include @@ -479,8 +481,31 @@ ::mstch::node CXXRecord::methods() method_vector.back()->setLast(true); // Copy each template into the mstch template array. - for (auto method_template : method_vector) - method_templates.push_back(method_template); + for (const std::shared_ptr &method : method_vector) + method_templates.push_back(method); + + // Collect all the non-static method names + std::unordered_set non_static_method_names; + for (const std::shared_ptr &method : method_vector) + { + if (!method->isStaticAsBool()) + non_static_method_names.insert(method->nameAsString()); + } + + // Set each static member whether it has the same name of the instance + // methods in the same class, which is not allowed by Python. How to handle + // the name conflict is determined by the configuration (see also + // CompiledConfiguration::StaticMethodNamePolicy). + for (const std::shared_ptr &method : method_vector) + { + if (method->isStaticAsBool()) + { + auto it = non_static_method_names.find(method->nameAsString()); + if (it != non_static_method_names.end()) + method->setNameConflict(true); + } + } + return method_templates; } @@ -968,8 +993,11 @@ ::mstch::node Function::isTemplate() } Method::Method(const ::chimera::CompiledConfiguration &config, - const CXXMethodDecl *decl, const CXXRecordDecl *class_decl) - : Function(config, decl, class_decl), method_decl_(decl) + const CXXMethodDecl *decl, const CXXRecordDecl *class_decl, + const int argument_limit) + : Function(config, decl, class_decl, argument_limit) + , method_decl_(decl) + , name_conflict_(false) { register_methods(this, { {"is_const", &Method::isConst}, @@ -979,16 +1007,88 @@ Method::Method(const ::chimera::CompiledConfiguration &config, }); } +void Method::setNameConflict(bool val) +{ + name_conflict_ = val; +} + +bool Method::isNameConflict() const +{ + return name_conflict_; +} + +::mstch::node Method::overloads() +{ + ::mstch::array overloads; + + // Create a list of wrappers that re-wrap this function with a subset of + // the full set of arguments (i.e. omitting some of the default arguments). + auto arg_range = chimera::util::getFunctionArgumentRange(decl_); + for (unsigned n_args = arg_range.first; n_args < arg_range.second; ++n_args) + { + auto method = std::make_shared(config_, method_decl_, + class_decl_, n_args); + method->setNameConflict(name_conflict_); + overloads.push_back(method); + } + + // Add this function to its own list of overloads. + // It can be distinguished from other copies because `uses_defaults=false`. + overloads.push_back(shared_from_this()); + + return overloads; +} + +std::string Method::nameAsString() +{ + std::string original_name = Function::nameAsString(); + + // TODO: This shouldn't happen + if (original_name.empty()) + return original_name; + + if (isStaticAsBool() && name_conflict_) + { + switch (config_.GetStaticMethodNamePolicy()) + { + case CompiledConfiguration::StaticMethodNamePolicy::NO_CHANGE: + // Do nothing + break; + case CompiledConfiguration::StaticMethodNamePolicy::TO_UPPER: + original_name = chimera::util::toUpper(original_name); + break; + case CompiledConfiguration::StaticMethodNamePolicy::TO_LOWER: + original_name = chimera::util::toLower(original_name); + break; + case CompiledConfiguration::StaticMethodNamePolicy::TO_PASCAL: + original_name = chimera::util::toPascal(original_name); + break; + case CompiledConfiguration::StaticMethodNamePolicy::TO_CAMEL: + original_name = chimera::util::toCamel(original_name); + break; + default: + break; + } + } + + return original_name; +} + ::mstch::node Method::isConst() { return method_decl_->isConst(); } -::mstch::node Method::isStatic() +bool Method::isStaticAsBool() const { return method_decl_->isStatic(); } +::mstch::node Method::isStatic() +{ + return isStaticAsBool(); +} + ::mstch::node Method::isVirtual() { return method_decl_->isVirtual(); diff --git a/src/util.cpp b/src/util.cpp index bd32732..aa0da79 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -955,6 +955,95 @@ std::string trim(std::string s, const char *t) return trimLeft(trimRight(s, t), t); } +std::string toUpper(const std::string &str, int n) +{ + std::string casted = str; + if (n < 0 || static_cast(n) >= casted.length()) + { + std::transform(casted.begin(), casted.end(), casted.begin(), ::toupper); + } + else + { + std::transform(casted.begin(), casted.begin() + n, casted.begin(), + ::toupper); + } + return casted; +} + +std::string toLower(const std::string &str, int n) +{ + std::string casted = str; + if (n < 0 || static_cast(n) >= casted.length()) + { + std::transform(casted.begin(), casted.end(), casted.begin(), ::tolower); + } + else + { + std::transform(casted.begin(), casted.begin() + n, casted.begin(), + ::tolower); + } + return casted; +} + +std::string toPascal(const std::string &str) +{ + auto tokens = split(str, "_"); + for (auto &token : tokens) + token = toUpper(token, 1); + + std::string casted; + for (const auto &token : tokens) + casted += token; + + return casted; +} + +std::string toCamel(const std::string &str) +{ + auto tokens = split(str, "_"); + + bool first_token_found = false; + for (auto &token : tokens) + { + if (!first_token_found) + { + if (token.empty()) + continue; + + token = toLower(token, 1); + first_token_found = true; + continue; + } + + token = toUpper(token, 1); + } + + std::string casted; + for (const auto &token : tokens) + casted += token; + + return casted; +} + +std::vector split(const std::string &str, + const std::string &delimiter) +{ + std::size_t pos_start = 0; + std::size_t pos_end, delim_len = delimiter.length(); + std::string token; + std::vector tokens; + + while ((pos_end = str.find(delimiter, pos_start)) != std::string::npos) + { + token = str.substr(pos_start, pos_end - pos_start); + pos_start = pos_end + delim_len; + tokens.push_back(token); + } + + tokens.push_back(str.substr(pos_start)); + return tokens; +} + bool startsWith(const std::string &str, const std::string &prefix) { return ((prefix.size() <= str.size()) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 013e791..dd1567d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -70,6 +70,7 @@ endfunction() # Add tests #=============================================================================== chimera_add_test(test_emulator) +chimera_add_test(test_util) # Add custom target to build all the tests as a single target get_property(chimera_cpp_tests GLOBAL PROPERTY CHIMERA_CPP_TESTS) diff --git a/test/examples/02_class/class.h b/test/examples/02_class/class.h index 4f05e08..476086d 100644 --- a/test/examples/02_class/class.h +++ b/test/examples/02_class/class.h @@ -99,4 +99,28 @@ class ClassInDetail } // namespace detail +namespace test1 +{ + +class Integer +{ +public: + Integer(int val) : m_val(val) + { + } + int add(int a) + { + return a + m_val; + } + static int add(int a, int b, int c = 0) + { + return a + b + c; + } + +private: + int m_val; +}; + +} // namespace test1 + } // namespace chimera_test diff --git a/test/examples/02_class/class.py b/test/examples/02_class/class.py index 114c435..d309636 100644 --- a/test/examples/02_class/class.py +++ b/test/examples/02_class/class.py @@ -10,92 +10,60 @@ import class_boost_python as boost -class TestFunction(unittest.TestCase): +class TestClass(unittest.TestCase): - def test_function_py11(self): - if not has_pybind11: - return - - dog = py11.Dog() - self.assertEqual(dog.type(), 'Dog') - self.assertEqual(dog.pure_virtual_type(), 'Dog') - self.assertEqual(py11.Dog.static_type(), 'Dog') - - husky = py11.Husky() - self.assertEqual(husky.type(), 'Husky') - self.assertEqual(husky.pure_virtual_type(), 'Husky') - - strong_husky = py11.StrongHusky() - self.assertEqual(strong_husky.type(), 'StrongHusky') - self.assertEqual(strong_husky.pure_virtual_type(), 'StrongHusky') - - default_args = py11.DefaultArguments() - self.assertEqual(default_args.add(), 3) - self.assertEqual(default_args.add(2), 4) - self.assertEqual(default_args.add(2, 3), 5) - - self.assertEqual(py11.StaticFields.m_static_readonly_type, 'static readonly type') - self.assertEqual(py11.StaticFields.m_static_readwrite_type, 'static readwrite type') - py11.StaticFields.m_static_readwrite_type = 'new type' - self.assertEqual(py11.StaticFields.m_static_readwrite_type, 'new type') - self.assertEqual(py11.StaticFields.static_type(), 'static type') - - non_pub_param_in_ctr = py11.NonPublicParamInConstructor('Herb') - self.assertEqual(non_pub_param_in_ctr.m_name, 'Herb') - - # Check if the main class and the nested class can be created w/o errors - mc = py11.MainClass() - nc = py11.MainClass.NestedClass() - - # MainClass shouldn't be a module - self.assertFalse(inspect.ismodule(py11.MainClass)) - self.assertTrue(inspect.isclass(py11.MainClass)) - self.assertTrue(inspect.isclass(py11.MainClass.NestedClass)) - - import class_pybind11 - self.assertFalse(hasattr(class_pybind11, 'detail')) - - def test_function_bp(self): - dog = boost.Dog() + def _test_class(self, binding): + dog = binding.Dog() self.assertEqual(dog.type(), 'Dog') self.assertEqual(dog.pure_virtual_type(), 'Dog') - self.assertEqual(boost.Dog.static_type(), 'Dog') + self.assertEqual(binding.Dog.static_type(), 'Dog') - husky = boost.Husky() + husky = binding.Husky() self.assertEqual(husky.type(), 'Husky') self.assertEqual(husky.pure_virtual_type(), 'Husky') - strong_husky = boost.StrongHusky() + strong_husky = binding.StrongHusky() self.assertEqual(strong_husky.type(), 'StrongHusky') self.assertEqual(strong_husky.pure_virtual_type(), 'StrongHusky') - default_args = boost.DefaultArguments() + default_args = binding.DefaultArguments() self.assertEqual(default_args.add(), 3) self.assertEqual(default_args.add(2), 4) self.assertEqual(default_args.add(2, 3), 5) - self.assertEqual(boost.StaticFields.m_static_readonly_type, 'static readonly type') - self.assertEqual(boost.StaticFields.m_static_readwrite_type, 'static readwrite type') + self.assertEqual(binding.StaticFields.m_static_readonly_type, 'static readonly type') + self.assertEqual(binding.StaticFields.m_static_readwrite_type, 'static readwrite type') # TODO(JS): Boost.Python doesn't work for readwrite static field (see: #194) - # boost.StaticFields.m_static_readwrite_type = 'new type' - # self.assertEqual(boost.StaticFields.m_static_readwrite_type, 'new type') - self.assertEqual(boost.StaticFields.static_type(), 'static type') + if has_pybind11 and binding is py11: + binding.StaticFields.m_static_readwrite_type = 'new type' + self.assertEqual(binding.StaticFields.m_static_readwrite_type, 'new type') + self.assertEqual(binding.StaticFields.static_type(), 'static type') - non_pub_param_in_ctr = boost.NonPublicParamInConstructor('Herb') + non_pub_param_in_ctr = binding.NonPublicParamInConstructor('Herb') self.assertEqual(non_pub_param_in_ctr.m_name, 'Herb') # Check if the main class and the nested class can be created w/o errors - mc = boost.MainClass() - nc = boost.MainClass.NestedClass() + mc = binding.MainClass() + nc = binding.MainClass.NestedClass() # MainClass shouldn't be a module - self.assertFalse(inspect.ismodule(boost.MainClass)) - self.assertTrue(inspect.isclass(boost.MainClass)) - self.assertTrue(inspect.isclass(boost.MainClass.NestedClass)) - - import class_boost_python - self.assertFalse(hasattr(class_boost_python, 'detail')) - + self.assertFalse(inspect.ismodule(binding.MainClass)) + self.assertTrue(inspect.isclass(binding.MainClass)) + self.assertTrue(inspect.isclass(binding.MainClass.NestedClass)) + + self.assertFalse(hasattr(binding, 'detail')) + + # Call static method + self.assertEqual(binding.test1.Integer.Add(1, 2), 3) + + # Call instance method + i = binding.test1.Integer(1) + self.assertEqual(i.add(2), 3) + + def test_class(self): + self._test_class(boost) + if has_pybind11: + self._test_class(py11) if __name__ == '__main__': unittest.main() diff --git a/test/examples/02_class/class.yaml b/test/examples/02_class/class.yaml index 477d231..59104da 100644 --- a/test/examples/02_class/class.yaml +++ b/test/examples/02_class/class.yaml @@ -2,7 +2,9 @@ arguments: - "-extra-arg" - "-I/usr/lib/clang/3.6/include" +options: + static_method_name_policy: "to_pascal" namespaces: - 'chimera_test': + "chimera_test": name: null # TODO: otherwise, import error - 'chimera_test::detail': null + "chimera_test::detail": null diff --git a/test/test_util.cpp b/test/test_util.cpp new file mode 100644 index 0000000..a420632 --- /dev/null +++ b/test/test_util.cpp @@ -0,0 +1,49 @@ +#include +#include "chimera/util.h" + +using namespace chimera; + +//============================================================================== +TEST(Util, StringCast) +{ + // Test toUpper + EXPECT_EQ(util::toUpper("Illusory_or_Impossible"), + "ILLUSORY_OR_IMPOSSIBLE"); + EXPECT_EQ(util::toUpper("Illusory_or_Impossible", -1), + "ILLUSORY_OR_IMPOSSIBLE"); + EXPECT_EQ(util::toUpper("Illusory_or_Impossible", 0), + "Illusory_or_Impossible"); + EXPECT_EQ(util::toUpper("Illusory_or_Impossible", 2), + "ILlusory_or_Impossible"); + + // Test toLower + EXPECT_EQ(util::toLower("Illusory_or_Impossible"), + "illusory_or_impossible"); + EXPECT_EQ(util::toLower("Illusory_or_Impossible", -1), + "illusory_or_impossible"); + EXPECT_EQ(util::toLower("Illusory_or_Impossible", 0), + "Illusory_or_Impossible"); + EXPECT_EQ(util::toLower("Illusory_or_Impossible", 2), + "illusory_or_Impossible"); + + // Test toPascal + EXPECT_EQ(util::toPascal("illusory_or_impossible"), "IllusoryOrImpossible"); + EXPECT_EQ(util::toPascal("illusoryOrImpossible"), "IllusoryOrImpossible"); + EXPECT_EQ(util::toPascal("IllusoryOrImpossible"), "IllusoryOrImpossible"); + EXPECT_EQ(util::toPascal("illusory_or__impossible"), + "IllusoryOrImpossible"); + EXPECT_EQ(util::toPascal("illusory_or_impossible___"), + "IllusoryOrImpossible"); + EXPECT_EQ(util::toPascal("____illusory_or_impossible"), + "IllusoryOrImpossible"); + + // Test toCamel + EXPECT_EQ(util::toCamel("illusory_or_impossible"), "illusoryOrImpossible"); + EXPECT_EQ(util::toCamel("illusoryOrImpossible"), "illusoryOrImpossible"); + EXPECT_EQ(util::toCamel("IllusoryOrImpossible"), "illusoryOrImpossible"); + EXPECT_EQ(util::toCamel("illusory_or__impossible"), "illusoryOrImpossible"); + EXPECT_EQ(util::toCamel("illusory_or_impossible___"), + "illusoryOrImpossible"); + EXPECT_EQ(util::toCamel("____illusory_or_impossible"), + "illusoryOrImpossible"); +}