diff --git a/include/cppast/cpp_entity.hpp b/include/cppast/cpp_entity.hpp index 639c61b..8f22349 100644 --- a/include/cppast/cpp_entity.hpp +++ b/include/cppast/cpp_entity.hpp @@ -65,6 +65,11 @@ public: return name_; } + void set_name(const std::string& name) const noexcept + { + this->name_ = name; + } + /// \returns The name of the new scope created by the entity, /// if there is any. type_safe::optional scope_name() const @@ -170,7 +175,7 @@ private: parent_ = type_safe::ref(parent); } - std::string name_; + mutable std::string name_; std::string comment_; cpp_attribute_list attributes_; type_safe::optional_ref parent_; diff --git a/tool/CMakeLists.txt b/tool/CMakeLists.txt index e6179ec..1cfd543 100644 --- a/tool/CMakeLists.txt +++ b/tool/CMakeLists.txt @@ -7,3 +7,9 @@ target_link_libraries(cppast_tool PUBLIC cppast cxxopts) set_target_properties(cppast_tool PROPERTIES CXX_STANDARD 11 OUTPUT_NAME cppast) install(TARGETS cppast_tool) + +add_executable(cppcgen cppcgen.cpp) +target_link_libraries(cppcgen PUBLIC cppast cxxopts) +set_target_properties(cppcgen PROPERTIES CXX_STANDARD 11 OUTPUT_NAME cppcgen) + +install(TARGETS cppcgen) diff --git a/tool/cppcgen.cpp b/tool/cppcgen.cpp new file mode 100644 index 0000000..3017472 --- /dev/null +++ b/tool/cppcgen.cpp @@ -0,0 +1,475 @@ +// Copyright (C) 2017-2022 Jonathan Müller and cppast contributors +// SPDX-License-Identifier: MIT + +#include "cppast/cpp_member_function.hpp" +#include + +#include + +#include // for generate_code() +#include // for generate_code() +#include // for the cpp_entity_kind definition +#include // for is_definition() +#include // for cpp_namespace +#include // for libclang_parser, libclang_compile_config, cpp_entity,... +#include // for visit() + +std::string CLASS_SUFFIX = "Ext"; +std::string FUNC_SUFFIX = "Func"; +std::string FUNC_VAR_PREFIX = "m_"; + +bool is_excluded_synopsis(const cppast::cpp_entity& e, cppast::cpp_access_specifier_kind access) +{ + // exclude privates and those marked for exclusion + return access == cppast::cpp_private || cppast::has_attribute(e, "documentation::exclude"); +} + +// print help options +void print_help(const cxxopts::Options& options) +{ + std::cout << options.help({"", "compilation"}) << '\n'; +} + +// print error message +void print_error(const std::string& msg) +{ + std::cerr << msg << '\n'; +} + +std::string generate_synopsis(const cppast::cpp_entity& e) +{ + // the generator for the synopsis + class synopsis_generator final : public cppast::code_generator + { + public: + // get the resulting string + std::string result() + { + return std::move(str_); + } + + private: + // whether or not the entity is the main entity that is being documented + bool is_main_entity(const cppast::cpp_entity& e) + { + if (cppast::is_templated(e) || cppast::is_friended(e)) + // need to ask the real entity + return is_main_entity(e.parent().value()); + else + return &e == &this->main_entity(); + } + + // get some nicer formatting + cppast::formatting do_get_formatting() const override + { + return cppast::formatting_flags::brace_nl | cppast::formatting_flags::comma_ws + | cppast::formatting_flags::operator_ws; + } + + // calculate generation options + generation_options do_get_options(const cppast::cpp_entity& e, + cppast::cpp_access_specifier_kind access) override + { + if (!is_main_entity(e)) + // only generation declaration for the non-documented entity + return cppast::code_generator::declaration; + else + // default options + return {}; + } + + // update indendation level + void do_indent() override + { + ++indent_; + } + void do_unindent() override + { + if (indent_) + --indent_; + } + + // write specified tokens + // need to change indentation for each newline + void do_write_token_seq(cppast::string_view tokens) override + { + if (was_newline_) + { + str_ += std::string(indent_ * 2u, ' '); + was_newline_ = false; + } + + str_ += tokens.c_str(); + } + + // write + remember newline + void do_write_newline() override + { + str_ += "\n"; + was_newline_ = true; + } + + std::string str_; + unsigned indent_ = 0; + bool was_newline_ = false; + } generator; + cppast::generate_code(generator, e); + return generator.result(); +} + +// trim from end (in place) +static inline void rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), s.end()); +} + +void output_member_function_typedef(std::stringstream& out, const cppast::cpp_entity& e, std::string& current_class) { + if (e.kind() != cppast::cpp_entity_kind::member_function_t) { + return; + } + + auto& mf = static_cast(e); + out << "typedef "; + out << cppast::to_string(mf.return_type()) << " "; + out << "(*"<< current_class << mf.name() << FUNC_SUFFIX << ")(const " << current_class << "*" << " self"; + int i = 0; + for (auto& param : mf.parameters()) { + if (i == 0) { + // add comma if there are parameters + out << ", "; + } + out << ( i ? ", " : "" ); + out << cppast::to_string(param.type()) << " " << param.name(); + i++; + } + out << ");\n"; +} + +void output_member_function(std::stringstream& out, const cppast::cpp_entity& e, std::string& prefix, std::string& base_class, std::string& current_class) { + if (e.kind() != cppast::cpp_entity_kind::member_function_t) return; + + auto& mf = static_cast(e); + int j = 0; + for (auto& param : mf.parameters()) { + if (param.name().empty()) { + param.set_name("param" + std::to_string(j)); + } + j++; + } + + std::string method_stub = generate_synopsis(e); + rtrim(method_stub); + method_stub.pop_back(); // get rid of semicolon + + std::string mvar_name = FUNC_VAR_PREFIX + mf.name(); + std::string func_type = current_class + mf.name() + FUNC_SUFFIX; + std::stringstream all_params_stream; + + int i = 0; + for (auto& param : mf.parameters()) { + all_params_stream << ( i ? ", " : "" ); + all_params_stream << param.name(); + i++; + } + std::string all_params = all_params_stream.str(); + + // generate class var + out << prefix << func_type << " " << mvar_name << ";\n"; + // TODO: Generate class var accessors (Get and Set) + // Maybe don't need to? + + bool is_override = cppast::is_overriding(mf.virtual_info()); + // generate method + out << prefix << method_stub; + if (method_stub.find("override") == std::string::npos || !is_override) { + out << " override"; + } + out << "\n"; + out << prefix << "{\n"; + out << prefix << prefix; + + std::string ret_type = cppast::to_string(mf.return_type()); + bool is_void = ret_type == "void"; + + // call super + if (!is_void) { + out << ret_type << " res = "; + } + out << base_class << "::" << mf.name() << "(" << all_params << ");\n"; + out << prefix << prefix; + + out << "if (" << mvar_name << "){\n"; + out << prefix << prefix << prefix; + out << "return "; + if (i > 0) { + out << mvar_name << "(this, " << all_params << ");\n"; + } + else { + out << mvar_name << "(this);\n"; + } + out << prefix << prefix; + out << "}\n"; // endif + if (!is_void) { + out << prefix << prefix; + out << "else {\n"; + out << prefix << prefix << prefix; + out << "return res;\n"; + out << prefix << prefix; + out << "}\n"; + + } + + out << prefix << "}\n"; +} + +// prints the AST of a file +void print_ast(std::ostream& out, const cppast::cpp_file& file) +{ + // print file name + std::cerr << "AST for '" << file.name() << "':\n"; + out << "#include \n#include \n\n"; + std::string prefix; // the current prefix string + std::string current_class; // the current class + std::string base_class; // the current base class + + std::stringstream forward_decls; + std::stringstream class_stream; + + // - Forward declare the class + // - typedef all of the virtual methods with class as first arg + // - declare the class with a postfix (class wxAppC: public wxApp) + // - output variable and virtual method override + // check function pointer for null, then call it + // + cppast::visit(file, + [](const cppast::cpp_entity& e, cppast::cpp_access_specifier_kind access) { + // only visit non-templated class definitions + return (!cppast::is_templated(e) && + access != cppast::cpp_private && + ( + (e.kind() == cppast::cpp_entity_kind::class_t && cppast::is_definition(e)) || + (e.kind() == cppast::cpp_entity_kind::member_function_t && static_cast(e).is_virtual()) || + (e.kind() == cppast::cpp_entity_kind::constructor_t) || + (e.kind() == cppast::cpp_entity_kind::destructor_t && static_cast(e).is_virtual()) || + e.kind() == cppast::cpp_entity_kind::base_class_t + ) + ); + }, + + [&](const cppast::cpp_entity& e, cppast::visitor_info info) { + if (info.event == cppast::visitor_info::container_entity_enter) { + if (e.kind() == cppast::cpp_entity_kind::class_t) { + base_class = e.name(); + current_class = e.name() + CLASS_SUFFIX; + forward_decls << "class " << current_class << ";\n"; + + class_stream << "class " << current_class << ": public " << base_class; + class_stream << "\n{\n"; + class_stream << "public:\n"; + } + /* else if (e.kind() == cppast::cpp_entity_kind::member_function_t) { */ + + /* } */ + prefix += " "; + } + else if (info.event == cppast::visitor_info::container_entity_exit) + { + // we have visited all children of a container, + // remove prefix + prefix.pop_back(); + prefix.pop_back(); + if (e.kind() == cppast::cpp_entity_kind::class_t) { + class_stream << prefix << "};\n\n"; + } + } + else + { + if (e.kind() == cppast::cpp_entity_kind::member_function_t) { + auto& mf = static_cast(e); + int j = 0; + for (auto& param : mf.parameters()) { + if (param.name().empty()) { + param.set_name("param" + std::to_string(j)); + } + j++; + } + + output_member_function_typedef(forward_decls, e, current_class); + output_member_function(class_stream, e, prefix, base_class, current_class); + } + } + + return true; + }); + out << forward_decls.str() << "\n"; + out << class_stream.str(); + +} + +// parse a file +std::unique_ptr parse_file(const cppast::libclang_compile_config& config, + const cppast::diagnostic_logger& logger, + const std::string& filename, bool fatal_error) +{ + // the entity index is used to resolve cross references in the AST + // we don't need that, so it will not be needed afterwards + cppast::cpp_entity_index idx; + // the parser is used to parse the entity + // there can be multiple parser implementations + cppast::libclang_parser parser(type_safe::ref(logger)); + // parse the file + auto file = parser.parse(idx, filename, config); + if (fatal_error && parser.error()) + return nullptr; + return file; +} + +int main(int argc, char* argv[]) +try +{ + cxxopts::Options option_list("cppast", + "cppast - The commandline interface to the cppast library.\n"); + // clang-format off + option_list.add_options() + ("h,help", "display this help and exit") + ("version", "display version information and exit") + ("v,verbose", "be verbose when parsing") + ("fatal_errors", "abort program when a parser error occurs, instead of doing error correction") + ("file", "the file that is being parsed (last positional argument)", + cxxopts::value()); + option_list.add_options("compilation") + ("database_dir", "set the directory where a 'compile_commands.json' file is located containing build information", + cxxopts::value()) + ("database_file", "set the file name whose configuration will be used regardless of the current file name", + cxxopts::value()) + ("std", "set the C++ standard (c++98, c++03, c++11, c++14, c++1z (experimental), c++17, c++2a, c++20)", + cxxopts::value()->default_value(cppast::to_string(cppast::cpp_standard::cpp_latest))) + ("I,include_directory", "add directory to include search path", + cxxopts::value>()) + ("D,macro_definition", "define a macro on the command line", + cxxopts::value>()) + ("U,macro_undefinition", "undefine a macro on the command line", + cxxopts::value>()) + ("f,feature", "enable a custom feature (-fXX flag)", + cxxopts::value>()) + ("gnu_extensions", "enable GNU extensions (equivalent to -std=gnu++XX)") + ("msvc_extensions", "enable MSVC extensions (equivalent to -fms-extensions)") + ("msvc_compatibility", "enable MSVC compatibility (equivalent to -fms-compatibility)") + ("fast_preprocessing", "enable fast preprocessing, be careful, this breaks if you e.g. redefine macros in the same file!") + ("remove_comments_in_macro", "whether or not comments generated by macro are kept, enable if you run into errors"); + // clang-format on + option_list.parse_positional("file"); + + auto options = option_list.parse(argc, argv); + if (options.count("help")) + print_help(option_list); + else if (options.count("version")) + { + std::cout << "cppast version " << CPPAST_VERSION_STRING << "\n"; + std::cout << "Copyright (C) Jonathan Müller 2017-2019 \n"; + std::cout << '\n'; + std::cout << "Using libclang version " << CPPAST_CLANG_VERSION_STRING << '\n'; + } + else if (!options.count("file") || options["file"].as().empty()) + { + print_error("missing file argument"); + return 1; + } + else + { + // the compile config stores compilation flags + cppast::libclang_compile_config config; + if (options.count("database_dir")) + { + cppast::libclang_compilation_database database( + options["database_dir"].as()); + if (options.count("database_file")) + config + = cppast::libclang_compile_config(database, + options["database_file"].as()); + else + config + = cppast::libclang_compile_config(database, options["file"].as()); + } + + if (options.count("verbose")) + config.write_preprocessed(true); + + if (options.count("fast_preprocessing")) + config.fast_preprocessing(true); + + if (options.count("remove_comments_in_macro")) + config.remove_comments_in_macro(true); + + if (options.count("include_directory")) + for (auto& include : options["include_directory"].as>()) + config.add_include_dir(include); + if (options.count("macro_definition")) + for (auto& macro : options["macro_definition"].as>()) + { + auto equal = macro.find('='); + auto name = macro.substr(0, equal); + if (equal == std::string::npos) + config.define_macro(std::move(name), ""); + else + { + auto def = macro.substr(equal + 1u); + config.define_macro(std::move(name), std::move(def)); + } + } + if (options.count("macro_undefinition")) + for (auto& name : options["macro_undefinition"].as>()) + config.undefine_macro(name); + if (options.count("feature")) + for (auto& name : options["feature"].as>()) + config.enable_feature(name); + + // the compile_flags are generic flags + cppast::compile_flags flags; + if (options.count("gnu_extensions")) + flags |= cppast::compile_flag::gnu_extensions; + if (options.count("msvc_extensions")) + flags |= cppast::compile_flag::ms_extensions; + if (options.count("msvc_compatibility")) + flags |= cppast::compile_flag::ms_compatibility; + + if (options["std"].as() == "c++98") + config.set_flags(cppast::cpp_standard::cpp_98, flags); + else if (options["std"].as() == "c++03") + config.set_flags(cppast::cpp_standard::cpp_03, flags); + else if (options["std"].as() == "c++11") + config.set_flags(cppast::cpp_standard::cpp_11, flags); + else if (options["std"].as() == "c++14") + config.set_flags(cppast::cpp_standard::cpp_14, flags); + else if (options["std"].as() == "c++1z") + config.set_flags(cppast::cpp_standard::cpp_1z, flags); + else if (options["std"].as() == "c++17") + config.set_flags(cppast::cpp_standard::cpp_17, flags); + else if (options["std"].as() == "c++2a") + config.set_flags(cppast::cpp_standard::cpp_2a, flags); + else if (options["std"].as() == "c++20") + config.set_flags(cppast::cpp_standard::cpp_20, flags); + else + { + print_error("invalid value '" + options["std"].as() + "' for std flag"); + return 1; + } + + // the logger is used to print diagnostics + cppast::stderr_diagnostic_logger logger; + if (options.count("verbose")) + logger.set_verbose(true); + + auto file = parse_file(config, logger, options["file"].as(), + options.count("fatal_errors") == 1); + if (!file) + return 2; + print_ast(std::cout, *file); + } +} +catch (const cppast::libclang_error& ex) +{ + print_error(std::string("[fatal parsing error] ") + ex.what()); + return 2; +}