From 507cd27eafb3e7dcefde5479708da7df313658fe Mon Sep 17 00:00:00 2001 From: SirLynix Date: Tue, 21 Mar 2023 13:21:00 +0100 Subject: [PATCH] Start working on documentation generator --- documentation/.gitignore | 2 + documentation/generator/src/main.cpp | 369 ++++++++++++++++++ documentation/generator/xmake.lua | 8 + documentation/xmake.lua | 5 + .../Wrapper/EGL/EGLFunctions.hpp | 4 + .../Wrapper/DeviceFunctions.hpp | 4 + .../Wrapper/GlobalFunctions.hpp | 4 + .../Wrapper/InstanceFunctions.hpp | 4 + xmake.lua | 1 + xmake/actions/generatedoc.lua | 142 +++++++ 10 files changed, 543 insertions(+) create mode 100644 documentation/.gitignore create mode 100644 documentation/generator/src/main.cpp create mode 100644 documentation/generator/xmake.lua create mode 100644 documentation/xmake.lua create mode 100644 xmake/actions/generatedoc.lua diff --git a/documentation/.gitignore b/documentation/.gitignore new file mode 100644 index 000000000..183404936 --- /dev/null +++ b/documentation/.gitignore @@ -0,0 +1,2 @@ +docgen.json +generated/ diff --git a/documentation/generator/src/main.cpp b/documentation/generator/src/main.cpp new file mode 100644 index 000000000..053e19e67 --- /dev/null +++ b/documentation/generator/src/main.cpp @@ -0,0 +1,369 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +nlohmann::ordered_json buildClass(const std::string& scope, const cppast::cpp_class& classNode); + +class simpleCodeGenerator : public cppast::code_generator +{ + std::string str_; // the result + bool was_newline_ = false; // whether or not the last token was a newline + // needed for lazily printing them + +public: + simpleCodeGenerator(const cppast::cpp_entity& e) + { + // kickoff code generation here + cppast::generate_code(*this, e); + } + + // return the result + const std::string& str() const noexcept + { + return str_; + } + +private: + // called to retrieve the generation options of an entity + generation_options do_get_options(const cppast::cpp_entity&, + cppast::cpp_access_specifier_kind) override + { + // generate declaration only + return code_generator::declaration; + } + + // no need to handle indentation, as only a single line is used + void do_indent() override {} + void do_unindent() override {} + + // called when a generic token sequence should be generated + // there are specialized callbacks for various token kinds, + // to e.g. implement syntax highlighting + void do_write_token_seq(cppast::string_view tokens) override + { + if (was_newline_) + { + // lazily append newline as space + str_ += ' '; + was_newline_ = false; + } + // append tokens + str_ += tokens.c_str(); + } + + // called when a newline should be generated + // we're lazy as it will always generate a trailing newline, + // we don't want + void do_write_newline() override + { + was_newline_ = true; + } + +}; + +int main() +{ + nlohmann::ordered_json outputDoc; + nlohmann::ordered_json& moduleDoc = outputDoc["modules"]; + + cppast::libclang_compilation_database database("compile_commands"); + + cppast::libclang_compile_config config; + + Nz::MillisecondClock time; + + for (auto&& entry : std::filesystem::directory_iterator("../include/Nazara")) + { + if (!entry.is_directory()) + continue; + + std::string moduleName = Nz::PathToString(entry.path().filename()); + std::cout << "Parsing " << moduleName << " headers..." << std::endl; + + nlohmann::ordered_json& moduleEntryDoc = moduleDoc[moduleName]; + + // Use module source file as flags reference to parse this module headers + cppast::libclang_compile_config config(database, "src/Nazara/" + moduleName + "/" + moduleName + ".cpp"); + config.define_macro("NAZARA_DOCGEN", ""); + + cppast::stderr_diagnostic_logger logger; + //logger.set_verbose(true); + + std::vector threads; + std::mutex jsonMutex; + + for (auto&& headerFile : std::filesystem::recursive_directory_iterator(entry.path())) + { + if (!headerFile.is_regular_file() || headerFile.path().extension().generic_u8string() != ".hpp") + continue; + + std::string filepath = Nz::PathToString(headerFile.path()); + threads.push_back(std::thread([&, filepath] + { + std::cout << " - Parsing " + filepath + "...\n"; + + // 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 + try + { + auto file = parser.parse(idx, filepath, config); + if (parser.error()) + { + std::cerr << "failed to parse " << filepath << "\n"; + return; + } + + std::vector prefixes; + auto prefix = [&] + { + std::string p; + for (const std::string& prefix : prefixes) + { + p += prefix; + p += "::"; + } + + return p; + }; + + // visit each entity in the file + bool insideNazaraNamespace = false; + cppast::visit(*file, [&](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::file_t) + return true; + + if (!insideNazaraNamespace) + { + if (e.kind() != cppast::cpp_entity_kind::namespace_t) + return false; + + if (!prefixes.empty() || e.name() != "Nz") + return false; + + insideNazaraNamespace = true; + } + + bool shouldEnter = true; + switch (e.kind()) + { + case cppast::cpp_entity_kind::class_t: + { + auto& classNode = static_cast(e); + if (!classNode.is_definition()) + { + shouldEnter = false; + break; + } + + std::cout << "found " << cppast::to_string(classNode.class_kind()) << " " << prefix() << e.name() << std::endl; + nlohmann::ordered_json classDoc = buildClass(prefix(), classNode); + std::unique_lock lock(jsonMutex); + moduleEntryDoc["classes"].push_back(std::move(classDoc)); + break; + } + + case cppast::cpp_entity_kind::enum_t: + { + auto& enumNode = static_cast(e); + if (!enumNode.is_definition()) + { + shouldEnter = false; + break; + } + + std::cout << "found " << (enumNode.is_scoped() ? "enum class" : "enum") << " " << prefix() << e.name() << std::endl; + break; + } + + default: + break; + } + + prefixes.push_back(e.name()); + return shouldEnter; + } + else if (info.event == cppast::visitor_info::container_entity_exit) // exiting an old container + { + if (!prefixes.empty()) + { + prefixes.pop_back(); + if (prefixes.empty()) + insideNazaraNamespace = false; + } + } + + return true; + }); + } + catch (const cppast::libclang_error& err) + { + std::cerr << "failed to parse " << filepath << ": " << err.what() << "\n"; + } + })); + } + + for (std::thread& t : threads) + t.join(); + + auto& classArray = moduleEntryDoc["classes"]; + + std::sort(classArray.begin(), classArray.end(), [](const nlohmann::ordered_json& classA, const nlohmann::ordered_json& classB) + { + const std::string& nameA = classA["name"]; + const std::string& nameB = classB["name"]; + return nameA < nameB; + }); + } + + std::fstream outputFile("docgen.json", outputFile.trunc | outputFile.out); + outputFile << outputDoc.dump(1, '\t'); + + std::cout << "Generated documentation in " << time.GetElapsedTime() << std::endl; + + return EXIT_SUCCESS; +} + +nlohmann::ordered_json buildClass(const std::string& scope, const cppast::cpp_class& classNode) +{ + nlohmann::ordered_json classDoc; + classDoc["name"] = scope + classNode.name(); + + bool isPublic = classNode.class_kind() != cppast::cpp_class_kind::class_t; + for (const auto& e : classNode) + { + switch (e.kind()) + { + case cppast::cpp_entity_kind::access_specifier_t: + { + isPublic = static_cast(e).access_specifier() == cppast::cpp_access_specifier_kind::cpp_public; + break; + } + + case cppast::cpp_entity_kind::constructor_t: + { + if (!isPublic) + break; + + const auto& memberFunc = static_cast(e); + + auto& constructorDoc = classDoc["constructors"].emplace_back(); + constructorDoc["name"] = memberFunc.name(); + constructorDoc["signature"] = memberFunc.signature(); + constructorDoc["codeGen"] = simpleCodeGenerator(memberFunc).str(); + + auto& parameterArray = constructorDoc["parameters"]; + parameterArray = nlohmann::ordered_json::array(); + + for (const auto& parameter : memberFunc.parameters()) + { + auto& parameterDoc = parameterArray.emplace_back(); + parameterDoc["name"] = parameter.name(); + parameterDoc["type"] = cppast::to_string(parameter.type()); + //if (const auto& defaultOpt = parameter.default_value()) + // parameterDoc["default"] = std::string(simpleCodeGenerator(defaultOpt.value())); + } + break; + } + + case cppast::cpp_entity_kind::destructor_t: + break; + + case cppast::cpp_entity_kind::member_function_t: + { + if (!isPublic) + break; + + const auto& memberFunc = static_cast(e); + + auto& methodDoc = classDoc["methods"].emplace_back(); + methodDoc["name"] = memberFunc.name(); + methodDoc["signature"] = memberFunc.signature(); + methodDoc["codeGen"] = simpleCodeGenerator(memberFunc).str(); + methodDoc["returnType"] = cppast::to_string(memberFunc.return_type()); + + auto& parameterArray = methodDoc["parameters"]; + parameterArray = nlohmann::ordered_json::array(); + + for (const auto& parameter : memberFunc.parameters()) + { + auto& parameterDoc = parameterArray.emplace_back(); + parameterDoc["name"] = parameter.name(); + parameterDoc["type"] = cppast::to_string(parameter.type()); + //if (const auto& defaultOpt = parameter.default_value()) + // parameterDoc["default"] = std::string(simpleCodeGenerator(defaultOpt.value())); + } + break; + } + + case cppast::cpp_entity_kind::member_variable_t: + { + if (!isPublic) + break; + + const auto& memberVariable = static_cast(e); + + auto& variableDoc = classDoc["variables"].emplace_back(); + variableDoc["name"] = memberVariable.name(); + variableDoc["type"] = cppast::to_string(memberVariable.type()); + break; + } + + case cppast::cpp_entity_kind::function_t: + { + if (!isPublic) + break; + + const auto& memberFunc = static_cast(e); + + auto& methodDoc = classDoc["staticMethods"].emplace_back(); + methodDoc["name"] = memberFunc.name(); + methodDoc["signature"] = memberFunc.signature(); + methodDoc["codeGen"] = simpleCodeGenerator(memberFunc).str(); + methodDoc["returnType"] = cppast::to_string(memberFunc.return_type()); + + auto& parameterArray = methodDoc["parameters"]; + parameterArray = nlohmann::ordered_json::array(); + + for (const auto& parameter : memberFunc.parameters()) + { + auto& parameterDoc = parameterArray.emplace_back(); + parameterDoc["name"] = parameter.name(); + parameterDoc["type"] = cppast::to_string(parameter.type()); + //if (const auto& defaultOpt = parameter.default_value()) + // parameterDoc["default"] = std::string(simpleCodeGenerator(defaultOpt.value())); + } + break; + } + + default: + { + if (isPublic) + std::cerr << "ignored public " << cppast::to_string(e.kind()) << " " << e.name() << std::endl; + + break; + } + } + } + + return classDoc; +} \ No newline at end of file diff --git a/documentation/generator/xmake.lua b/documentation/generator/xmake.lua new file mode 100644 index 000000000..ef73506d8 --- /dev/null +++ b/documentation/generator/xmake.lua @@ -0,0 +1,8 @@ +add_requires("cppast", "nlohmann_json") + +target("docgen", function () + set_rundir("../") + add_files("src/*.cpp") + add_deps("NazaraCore") + add_packages("cppast", "nazarautils", "nlohmann_json") +end) diff --git a/documentation/xmake.lua b/documentation/xmake.lua new file mode 100644 index 000000000..c29a3673c --- /dev/null +++ b/documentation/xmake.lua @@ -0,0 +1,5 @@ +option("docgen", { description = "Enables documentation generator (requires LLVM)", default = false }) + +if has_config("docgen") then + includes("generator/xmake.lua") +end diff --git a/include/Nazara/OpenGLRenderer/Wrapper/EGL/EGLFunctions.hpp b/include/Nazara/OpenGLRenderer/Wrapper/EGL/EGLFunctions.hpp index 97a2af22c..a0ac53333 100644 --- a/include/Nazara/OpenGLRenderer/Wrapper/EGL/EGLFunctions.hpp +++ b/include/Nazara/OpenGLRenderer/Wrapper/EGL/EGLFunctions.hpp @@ -4,6 +4,8 @@ // no header guards +#ifndef NAZARA_DOCGEN + #if !defined(NAZARA_OPENGLRENDERER_EGL_FUNC) || !defined(NAZARA_OPENGLRENDERER_EGL_FUNC_OPT) #error You must define NAZARA_OPENGLRENDERER_EGL_FUNC and NAZARA_OPENGLRENDERER_EGL_FUNC_OPT before including this file #endif @@ -29,3 +31,5 @@ NAZARA_OPENGLRENDERER_EGL_FUNC_OPT(eglCreatePlatformWindowSurfaceEXT, PFNEGLCREA #undef NAZARA_OPENGLRENDERER_EGL_FUNC #undef NAZARA_OPENGLRENDERER_EGL_FUNC_OPT + +#endif // NAZARA_DOCGEN diff --git a/include/Nazara/VulkanRenderer/Wrapper/DeviceFunctions.hpp b/include/Nazara/VulkanRenderer/Wrapper/DeviceFunctions.hpp index feae3b3bf..5624945a7 100644 --- a/include/Nazara/VulkanRenderer/Wrapper/DeviceFunctions.hpp +++ b/include/Nazara/VulkanRenderer/Wrapper/DeviceFunctions.hpp @@ -4,6 +4,8 @@ // no header guards +#ifndef NAZARA_DOCGEN + #if !defined(NAZARA_VULKANRENDERER_DEVICE_FUNCTION) || !defined(NAZARA_VULKANRENDERER_DEVICE_CORE_EXT_FUNCTION) #error You must define NAZARA_VULKANRENDERER_DEVICE_FUNCTION and NAZARA_VULKANRENDERER_DEVICE_CORE_EXT_FUNCTION before including this file #endif @@ -192,3 +194,5 @@ NAZARA_VULKANRENDERER_INSTANCE_EXT_END() #undef NAZARA_VULKANRENDERER_DEVICE_OR_INSTANCE_FUNCTION #undef NAZARA_VULKANRENDERER_INSTANCE_EXT_BEGIN #undef NAZARA_VULKANRENDERER_INSTANCE_EXT_END + +#endif \ No newline at end of file diff --git a/include/Nazara/VulkanRenderer/Wrapper/GlobalFunctions.hpp b/include/Nazara/VulkanRenderer/Wrapper/GlobalFunctions.hpp index 50e73c80a..9574227e4 100644 --- a/include/Nazara/VulkanRenderer/Wrapper/GlobalFunctions.hpp +++ b/include/Nazara/VulkanRenderer/Wrapper/GlobalFunctions.hpp @@ -4,6 +4,8 @@ // no header guards +#ifndef NAZARA_DOCGEN + #if !defined(NAZARA_VULKANRENDERER_GLOBAL_FUNCTION) || !defined(NAZARA_VULKANRENDERER_GLOBAL_FUNCTION_OPT) #error You must define NAZARA_VULKANRENDERER_GLOBAL_FUNCTION and NAZARA_VULKANRENDERER_GLOBAL_FUNCTION_OPT before including this file #endif @@ -16,3 +18,5 @@ NAZARA_VULKANRENDERER_GLOBAL_FUNCTION_OPT(vkEnumerateInstanceVersion) #undef NAZARA_VULKANRENDERER_GLOBAL_FUNCTION #undef NAZARA_VULKANRENDERER_GLOBAL_FUNCTION_OPT + +#endif //NAZARA_DOCGEN \ No newline at end of file diff --git a/include/Nazara/VulkanRenderer/Wrapper/InstanceFunctions.hpp b/include/Nazara/VulkanRenderer/Wrapper/InstanceFunctions.hpp index 0c1a3250c..b808df475 100644 --- a/include/Nazara/VulkanRenderer/Wrapper/InstanceFunctions.hpp +++ b/include/Nazara/VulkanRenderer/Wrapper/InstanceFunctions.hpp @@ -4,6 +4,8 @@ // no header guards +#ifndef NAZARA_DOCGEN + #if !defined(NAZARA_VULKANRENDERER_INSTANCE_FUNCTION) || !defined(NAZARA_VULKANRENDERER_INSTANCE_CORE_EXT_FUNCTION) #error You must define NAZARA_VULKANRENDERER_INSTANCE_FUNCTION and NAZARA_VULKANRENDERER_INSTANCE_CORE_EXT_FUNCTION before including this file #endif @@ -113,3 +115,5 @@ NAZARA_VULKANRENDERER_INSTANCE_EXT_END() #undef NAZARA_VULKANRENDERER_INSTANCE_EXT_BEGIN #undef NAZARA_VULKANRENDERER_INSTANCE_EXT_END #undef NAZARA_VULKANRENDERER_INSTANCE_FUNCTION + +#endif \ No newline at end of file diff --git a/xmake.lua b/xmake.lua index c1196a8a3..1533a7ea4 100644 --- a/xmake.lua +++ b/xmake.lua @@ -442,3 +442,4 @@ includes("tools/*.lua") includes("tests/*.lua") includes("examples/*.lua") includes("plugins/*.lua") +includes("documentation/*.lua") diff --git a/xmake/actions/generatedoc.lua b/xmake/actions/generatedoc.lua new file mode 100644 index 000000000..14f41253d --- /dev/null +++ b/xmake/actions/generatedoc.lua @@ -0,0 +1,142 @@ +task("generate-doc") + +set_menu({ + -- Settings menu usage + usage = "xmake generate-doc [options]", + description = "Parses the docgen.json to generate documentation" +}) + +local log = false + +on_run(function () + import("core.base.json") + + local docgen = assert(json.decode(io.readfile("documentation/docgen.json"))) + local classFiles = {} + local typelinks = {} + for moduleName, module in pairs(docgen.modules) do + local folder = "documentation/generated" .. "/" .. moduleName + for _, class in pairs(module.classes) do + assert(class.name:startswith("Nz::")) + local classkey = class.name:sub(5) + local lastsep = classkey:lastof("::") + local classname = lastsep and classkey:sub(lastsep + 2) or classkey + + local filepath = folder .. "/" .. classkey:gsub("::", ".") .. ".md" + local classData = { + fullname = class.name, + filename = filepath, + classname = classname, + class = class + } + table.insert(classFiles, classData) + local classnames = {class.name, classkey} + + local link = string.format("`[`%s`](%s)`", classkey, filepath) + for _, name in ipairs(classnames) do + table.insert(typelinks, { + key = classData, + pattern = "(.?)(" .. name .. ")(.?)", + replacement = function (b, n, e) + if #b > 0 and not b:match("^[`<%s]") then + return + end + + if #e > 0 and not e:match("^[>&*%s]") then + return + end + + local r = {} + table.insert(r, b) + table.insert(r, "`" .. classkey .. "`") + table.insert(r, e) + + return table.concat(r) + end + }) + table.insert(typelinks, { + excludes = classData, + pattern = "(.?)(" .. name .. ")(.?)", + replacement = function (b, n, e) + if #b > 0 and not b:match("^[`<%s]") then + return + end + + if #e > 0 and not e:match("^[>&*%s]") then + return + end + + local r = {} + table.insert(r, b) + table.insert(r, link) + table.insert(r, e) + + return table.concat(r) + end + }) + end + end + end + + table.sort(classFiles, function (a, b) + return a.fullname < b.fullname + end) + + local function type(typeStr, key) + for _, link in pairs(typelinks) do + if link.key then + if link.key == key then + typeStr = typeStr:gsub(link.pattern, link.replacement) + end + elseif link.excludes ~= key then + typeStr = typeStr:gsub(link.pattern, link.replacement) + end + end + if typeStr:startswith("``") then + typeStr = typeStr:sub(3) + end + if typeStr:endswith("``") then + typeStr = typeStr:sub(1, -3) + end + return typeStr + end + + for _, classData in pairs(classFiles) do + os.mkdir(path.directory(classData.filename)) + print("generating " .. classData.fullname .. "...") + local file = assert(io.open(classData.filename, "w+")) + file:print("# %s", classData.fullname) + file:print("") + file:print("Class description") + file:print("") + file:print("## Constructors") + file:print("") + for _, constructor in pairs(classData.class.constructors) do + local params = {} + for _, param in pairs(constructor.parameters) do + if #param.name > 0 then + table.insert(params, type(param.type, classData) .. " " .. param.name) + else + table.insert(params, type(param.type, classData)) + end + end + file:print("- `%s(%s)`", classData.classname, table.concat(params, ", ")) + end + file:print("") + file:print("## Methods") + file:print("") + file:print("| Return type | Signature |") + file:print("| ----------- | --------- |") + for _, method in pairs(classData.class.methods) do + local params = {} + for _, param in pairs(method.parameters) do + if #param.name > 0 then + table.insert(params, type(param.type, classData) .. " " .. param.name) + else + table.insert(params, type(param.type, classData)) + end + end + file:print("| %s | `%s(%s)` |", type("`" .. method.returnType .. "`", classData), method.name, table.concat(params, ", ")) + end + end +end) \ No newline at end of file