diff --git a/emitters/yaml_base_emitter.cpp b/emitters/yaml_base_emitter.cpp index 46403f2..8e9acf1 100644 --- a/emitters/yaml_base_emitter.cpp +++ b/emitters/yaml_base_emitter.cpp @@ -330,6 +330,100 @@ bool yaml_base_emitter::check_scalar(const std::string& filepath, /**************************************************************************************************/ +bool yaml_base_emitter::check_scalar_array(const std::string& filepath, + const json& have_node, + const json& expected_node, + const std::string& nodepath, + json& merged_node, + const std::string& key) { + const auto notify = [&](const std::string& validate_message, + const std::string& update_message) { + check_notify(filepath, nodepath, key, validate_message, update_message); + }; + + const auto notify_fail = [&](const std::string& message) { + check_notify(filepath, nodepath, key, message, message); + }; + + if (!expected_node.count(key)) { + throw std::runtime_error("missing expected node?"); + } + + const json& expected = expected_node[key]; + + if (!expected.is_string()) { + throw std::runtime_error("expected type mismatch?"); + } + + const std::string& expected_scalar(expected); + json& result = merged_node[key]; + + if (!have_node.count(key)) { + if (expected_scalar == tag_value_deprecated_k) { + // deprecated key not present in have. Do nothing, no error. + return false; + } else { + notify("value missing", "value inserted"); + result = expected; + return true; + } + } + + const json& have = have_node[key]; + + if (have.is_string()) { + const std::string& have_scalar(have); + + if (expected_scalar == tag_value_missing_k && have_scalar == tag_value_missing_k) { + if (_mode == yaml_mode::validate) { + notify("value not documented", ""); + } + + return true; + } + + if (expected_scalar == tag_value_deprecated_k) { + notify("key is deprecated", "deprecated key removed"); + return true; + } + + if (expected_scalar == tag_value_optional_k && have_scalar == tag_value_optional_k) { + return false; + } + + if (hyde::is_tag(have_scalar)) { + notify("value is unexpected tag", + "value updated from `" + have_scalar + "` to `" + expected_scalar + "`"); + result = expected; // Replace unexpected tag + return true; + } + } + + if (!have.is_array()) { + notify_fail("value not an array; expected an array of scalar values"); + return true; + } + + result = have; + + // We have an array; make sure its elements are scalar + std::size_t index{0}; + bool failure{false}; + for (const auto& have_element : have) { + if (!have_element.is_string()) { + failure = true; + notify_fail("non-scalar array element at index " + std::to_string(index)); + } else if (hyde::is_tag(have_element)) { + failure = true; + notify_fail("invalid value at index " + std::to_string(index)); + } + } + + return failure; +} + +/**************************************************************************************************/ + bool yaml_base_emitter::check_object_array(const std::string& filepath, const json& have_node, const json& expected_node, @@ -404,7 +498,7 @@ bool yaml_base_emitter::check_object_array(const std::string& filepath, if (have_elements.count(object_key) == 0) { std::string count_str(std::to_string(count)); std::string message("object at index " + count_str + " has no key"); - notify(message + "; skipped", message); + notify(message, message + "; skipped"); // preserve object indices for merging below. Name is irrelevant as // long as it's unique. Prefix with '.' to prevent actual key // conflicts. @@ -430,16 +524,16 @@ bool yaml_base_emitter::check_object_array(const std::string& filepath, have_found_iter != have_map.end() && have_found_iter->first == expected_key; std::string index_str(std::to_string(index)); if (!have_found) { - notify("required object inserted at index " + index_str, - "missing required object at index " + index_str); + notify("missing required object at index " + index_str, + "required object inserted at index " + index_str); result_array.push_back(expected_object); failure = true; } else { std::size_t have_index = have_found_iter->second; if (have_index != index) { std::string have_index_str(std::to_string(have_index)); - notify("moved item at index " + have_index_str + " to index " + index_str, - "bad item location; have: " + have_index_str + ", expected: " + index_str); + notify("bad item location; have: " + have_index_str + ", expected: " + index_str, + "moved item at index " + have_index_str + " to index " + index_str); failure = true; } std::string nodepath = "['" + key + "'][" + index_str + "]"; @@ -455,7 +549,7 @@ bool yaml_base_emitter::check_object_array(const std::string& filepath, std::string expected_size(std::to_string(expected.size())); std::string message = "sequence size mismatch; have " + have_size + ", expected " + expected_size; - notify(message + "; fixed", message); + notify(message, message + "; fixed"); failure = true; } @@ -681,7 +775,8 @@ auto load_yaml(const boost::filesystem::path& path) try { bool yaml_base_emitter::reconcile(json expected, boost::filesystem::path root_path, - boost::filesystem::path path) { + boost::filesystem::path path, + json& out_reconciled) { bool failure{false}; /* begin hack */ { @@ -726,6 +821,8 @@ bool yaml_base_emitter::reconcile(json expected, json merged; std::tie(failure, merged) = merge(relative_path, have, expected); + out_reconciled = merged; + out_reconciled["documentation_path"] = relative_path; switch (_mode) { case hyde::yaml_mode::validate: { @@ -745,6 +842,8 @@ bool yaml_base_emitter::reconcile(json expected, } break; } } else { // file does not exist + out_reconciled = expected; + switch (_mode) { case hyde::yaml_mode::validate: { std::cerr << relative_path << ": required file does not exist\n"; diff --git a/emitters/yaml_base_emitter.hpp b/emitters/yaml_base_emitter.hpp index 59caa15..acb5b36 100644 --- a/emitters/yaml_base_emitter.hpp +++ b/emitters/yaml_base_emitter.hpp @@ -54,15 +54,16 @@ struct yaml_base_emitter { public: yaml_base_emitter(boost::filesystem::path src_root, boost::filesystem::path dst_root, - yaml_mode mode) - : _src_root(std::move(src_root)), _dst_root(std::move(dst_root)), _mode(mode) {} + yaml_mode mode, + emit_options options) + : _src_root(std::move(src_root)), _dst_root(std::move(dst_root)), _mode(mode), _options(std::move(options)) {} - virtual bool emit(const json& json) = 0; + virtual bool emit(const json& j, json& out_emitted) = 0; protected: json base_emitter_node(std::string layout, std::string title, std::string tag); - bool reconcile(json node, boost::filesystem::path root_path, boost::filesystem::path path); + bool reconcile(json node, boost::filesystem::path root_path, boost::filesystem::path path, json& out_reconciled); std::string defined_in_file(const std::string& src_path, const boost::filesystem::path& src_root); @@ -89,6 +90,13 @@ struct yaml_base_emitter { json& out_merged, const std::string& key); + bool check_scalar_array(const std::string& filepath, + const json& have_node, + const json& expected_node, + const std::string& nodepath, + json& merged_node, + const std::string& key); + using check_proc = std::function + /**************************************************************************************************/ namespace hyde { @@ -24,6 +27,38 @@ static constexpr char const* index_filename_k = "index.md"; /**************************************************************************************************/ +enum class attribute_category { + disabled, + required, + optional, + deprecated +}; + +static constexpr char const* get_tag(attribute_category c) { + switch (c) { + case attribute_category::required: + return tag_value_missing_k; + case attribute_category::optional: + return tag_value_optional_k; + case attribute_category::deprecated: + return tag_value_deprecated_k; + default: + throw std::invalid_argument("unexpected attribute category"); + } +} + +static inline bool is_tag(const std::string& s) { + return s.substr(0, 2) == "__"; +} + +/**************************************************************************************************/ + +struct emit_options { + attribute_category _tested_by; +}; + +/**************************************************************************************************/ + } // namespace hyde /**************************************************************************************************/ diff --git a/emitters/yaml_class_emitter.cpp b/emitters/yaml_class_emitter.cpp index 667c9b2..2ef4834 100644 --- a/emitters/yaml_class_emitter.cpp +++ b/emitters/yaml_class_emitter.cpp @@ -79,7 +79,7 @@ bool yaml_class_emitter::do_merge(const std::string& filepath, /**************************************************************************************************/ -bool yaml_class_emitter::emit(const json& j) { +bool yaml_class_emitter::emit(const json& j, json& out_emitted) { json node = base_emitter_node("class", j["name"], "class"); node["defined-in-file"] = defined_in_file(j["defined-in-file"], _src_root); maybe_annotate(j, node); @@ -110,14 +110,16 @@ bool yaml_class_emitter::emit(const json& j) { auto dst = dst_path(j, static_cast(j["name"])); - bool failure = reconcile(std::move(node), _dst_root, std::move(dst) / index_filename_k); + bool failure = reconcile(std::move(node), _dst_root, std::move(dst) / index_filename_k, out_emitted); const auto& methods = j["methods"]; - yaml_function_emitter function_emitter(_src_root, _dst_root, _mode, true); + yaml_function_emitter function_emitter(_src_root, _dst_root, _mode, _options, true); for (auto it = methods.begin(); it != methods.end(); ++it) { function_emitter.set_key(it.key()); - failure |= function_emitter.emit(it.value()); + auto function_emitted = hyde::json::object(); + failure |= function_emitter.emit(it.value(), function_emitted); + out_emitted["methods"].push_back(std::move(function_emitted)); } return failure; diff --git a/emitters/yaml_class_emitter.hpp b/emitters/yaml_class_emitter.hpp index b61e7d5..bf70b64 100644 --- a/emitters/yaml_class_emitter.hpp +++ b/emitters/yaml_class_emitter.hpp @@ -24,10 +24,11 @@ namespace hyde { struct yaml_class_emitter : public yaml_base_emitter { explicit yaml_class_emitter(boost::filesystem::path src_root, boost::filesystem::path dst_root, - yaml_mode mode) - : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode) {} + yaml_mode mode, + emit_options options) + : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode, std::move(options)) {} - bool emit(const json& json) override; + bool emit(const json& j, json& out_emitted) override; bool do_merge(const std::string& filepath, const json& have, diff --git a/emitters/yaml_enum_emitter.cpp b/emitters/yaml_enum_emitter.cpp index 10e6b23..37b4d7a 100644 --- a/emitters/yaml_enum_emitter.cpp +++ b/emitters/yaml_enum_emitter.cpp @@ -42,7 +42,7 @@ bool yaml_enum_emitter::do_merge(const std::string& filepath, /**************************************************************************************************/ -bool yaml_enum_emitter::emit(const json& j) { +bool yaml_enum_emitter::emit(const json& j, json& out_emitted) { const std::string& name = j["name"]; // Most likely an enum forward declaration. Nothing to document here. @@ -65,7 +65,7 @@ bool yaml_enum_emitter::emit(const json& j) { node["values"].push_back(std::move(cur_value)); } - return reconcile(std::move(node), _dst_root, dst_path(j) / filename); + return reconcile(std::move(node), _dst_root, dst_path(j) / filename, out_emitted); } /**************************************************************************************************/ diff --git a/emitters/yaml_enum_emitter.hpp b/emitters/yaml_enum_emitter.hpp index 1b79cd3..fbb738c 100644 --- a/emitters/yaml_enum_emitter.hpp +++ b/emitters/yaml_enum_emitter.hpp @@ -24,10 +24,11 @@ namespace hyde { struct yaml_enum_emitter : public yaml_base_emitter { explicit yaml_enum_emitter(boost::filesystem::path src_root, boost::filesystem::path dst_root, - yaml_mode mode) - : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode) {} + yaml_mode mode, + emit_options options) + : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode, std::move(options)) {} - bool emit(const json& json) override; + bool emit(const json& j, json& out_emitted) override; bool do_merge(const std::string& filepath, const json& have, diff --git a/emitters/yaml_function_emitter.cpp b/emitters/yaml_function_emitter.cpp index 45929f9..9f908a3 100644 --- a/emitters/yaml_function_emitter.cpp +++ b/emitters/yaml_function_emitter.cpp @@ -38,6 +38,9 @@ bool yaml_function_emitter::do_merge(const std::string& filepath, failure |= check_scalar(filepath, have, expected, nodepath, out_merged, "description"); failure |= check_scalar(filepath, have, expected, nodepath, out_merged, "signature_with_names"); failure |= check_scalar(filepath, have, expected, nodepath, out_merged, "return"); + if (_options._tested_by != hyde::attribute_category::disabled) { + failure |= check_scalar_array(filepath, have, expected, nodepath, out_merged, "tested_by"); + } // failure |= check_scalar(filepath, have, expected, nodepath, out_merged, // "annotation"); @@ -65,7 +68,7 @@ bool yaml_function_emitter::do_merge(const std::string& filepath, /**************************************************************************************************/ -bool yaml_function_emitter::emit(const json& jsn) { +bool yaml_function_emitter::emit(const json& jsn, json& out_emitted) { boost::filesystem::path dst; std::string name; std::string filename; @@ -94,6 +97,9 @@ bool yaml_function_emitter::emit(const json& jsn) { // description is now optional when there is a singular variant. overloads[key]["description"] = count > 1 ? tag_value_missing_k : tag_value_optional_k; overloads[key]["return"] = tag_value_optional_k; + if (_options._tested_by != hyde::attribute_category::disabled) { + overloads[key]["tested_by"] = hyde::get_tag(_options._tested_by); + } maybe_annotate(overload, overloads[key]); if (!overload["arguments"].empty()) { @@ -122,7 +128,7 @@ bool yaml_function_emitter::emit(const json& jsn) { if (is_ctor) node["is_ctor"] = true; if (is_dtor) node["is_dtor"] = true; - return reconcile(std::move(node), _dst_root, dst / (filename + ".md")); + return reconcile(std::move(node), _dst_root, dst / (filename + ".md"), out_emitted); } /**************************************************************************************************/ diff --git a/emitters/yaml_function_emitter.hpp b/emitters/yaml_function_emitter.hpp index bea11dc..4a0140a 100644 --- a/emitters/yaml_function_emitter.hpp +++ b/emitters/yaml_function_emitter.hpp @@ -25,13 +25,14 @@ struct yaml_function_emitter : public yaml_base_emitter { explicit yaml_function_emitter(boost::filesystem::path src_root, boost::filesystem::path dst_root, yaml_mode mode, + emit_options options, bool as_methods) - : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode), + : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode, std::move(options)), _as_methods(as_methods) {} void set_key(std::string key) { _key = std::move(key); } - bool emit(const json& json) override; + bool emit(const json& j, json& out_emitted) override; bool do_merge(const std::string& filepath, const json& have, diff --git a/emitters/yaml_library_emitter.cpp b/emitters/yaml_library_emitter.cpp index c180c61..916af0a 100644 --- a/emitters/yaml_library_emitter.cpp +++ b/emitters/yaml_library_emitter.cpp @@ -36,14 +36,13 @@ bool yaml_library_emitter::do_merge(const std::string& filepath, /**************************************************************************************************/ -bool yaml_library_emitter::emit(const json&) { +bool yaml_library_emitter::emit(const json&, json& out_emitted) { json node = base_emitter_node("library", tag_value_missing_k, "library"); - node["library-type"] = "library"; node["icon"] = "book"; node["tab"] = tag_value_missing_k; - return reconcile(std::move(node), _dst_root, _dst_root / index_filename_k); + return reconcile(std::move(node), _dst_root, _dst_root / index_filename_k, out_emitted); } /**************************************************************************************************/ diff --git a/emitters/yaml_library_emitter.hpp b/emitters/yaml_library_emitter.hpp index 25c36ae..fb21e1d 100644 --- a/emitters/yaml_library_emitter.hpp +++ b/emitters/yaml_library_emitter.hpp @@ -25,10 +25,11 @@ namespace hyde { struct yaml_library_emitter : public yaml_base_emitter { explicit yaml_library_emitter(boost::filesystem::path src_root, boost::filesystem::path dst_root, - yaml_mode mode) - : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode) {} + yaml_mode mode, + emit_options options) + : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode, std::move(options)) {} - bool emit(const json& json) override; + bool emit(const json& j, json& out_emitted) override; bool do_merge(const std::string& filepath, const json& have, diff --git a/emitters/yaml_sourcefile_emitter.cpp b/emitters/yaml_sourcefile_emitter.cpp index 5f8762c..6922b84 100644 --- a/emitters/yaml_sourcefile_emitter.cpp +++ b/emitters/yaml_sourcefile_emitter.cpp @@ -52,7 +52,7 @@ bool yaml_sourcefile_emitter::do_merge(const std::string& filepath, /**************************************************************************************************/ -bool yaml_sourcefile_emitter::emit(const json& j) { +bool yaml_sourcefile_emitter::emit(const json& j, json& out_emitted) { const auto sub_path = subcomponent(static_cast(j["paths"]["src_path"]), _src_root); json node = base_emitter_node("library", sub_path.string(), "sourcefile"); node["library-type"] = "sourcefile"; @@ -61,7 +61,7 @@ bool yaml_sourcefile_emitter::emit(const json& j) { _sub_dst = dst_path(j, sub_path); - return reconcile(std::move(node), _dst_root, _sub_dst / index_filename_k); + return reconcile(std::move(node), _dst_root, _sub_dst / index_filename_k, out_emitted); } /**************************************************************************************************/ diff --git a/emitters/yaml_sourcefile_emitter.hpp b/emitters/yaml_sourcefile_emitter.hpp index d6871df..bd3f1c3 100644 --- a/emitters/yaml_sourcefile_emitter.hpp +++ b/emitters/yaml_sourcefile_emitter.hpp @@ -24,10 +24,11 @@ namespace hyde { struct yaml_sourcefile_emitter : public yaml_base_emitter { explicit yaml_sourcefile_emitter(boost::filesystem::path src_root, boost::filesystem::path dst_root, - yaml_mode mode) - : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode) {} + yaml_mode mode, + emit_options options) + : yaml_base_emitter(std::move(src_root), std::move(dst_root), mode, std::move(options)) {} - bool emit(const json& json) override; + bool emit(const json& j, json& out_emitted) override; bool do_merge(const std::string& filepath, const json& have, diff --git a/include/output_yaml.hpp b/include/output_yaml.hpp index 34342df..b156662 100644 --- a/include/output_yaml.hpp +++ b/include/output_yaml.hpp @@ -16,6 +16,7 @@ written permission of Adobe. // application #include "json_fwd.hpp" +#include "emitters/yaml_base_emitter_fwd.hpp" /**************************************************************************************************/ @@ -33,7 +34,9 @@ enum class yaml_mode { void output_yaml(json j, const boost::filesystem::path& src_root, const boost::filesystem::path& dst_root, - yaml_mode mode); + json& out_emitted, + yaml_mode mode, + const emit_options& options); /**************************************************************************************************/ diff --git a/sources/main.cpp b/sources/main.cpp index 0100be4..4175b4a 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -30,6 +30,7 @@ written permission of Adobe. #include "config.hpp" #include "json.hpp" #include "output_yaml.hpp" +#include "emitters/yaml_base_emitter_fwd.hpp" // instead of this, probably have a matcher manager that pushes the json object // into the file then does the collation and passes it into jsonAST to do @@ -116,9 +117,25 @@ static cl::opt ToolDiagnostic( clEnumValN(ToolDiagnosticVerbose, "hyde-verbose", "output more to the console"), clEnumValN(ToolDiagnosticVeryVerbose, "hyde-very-verbose", "output much more to the console")), cl::cat(MyToolCategory)); -static cl::opt YamlDstDir("hyde-yaml-dir", - cl::desc("Root directory for YAML validation / update"), - cl::cat(MyToolCategory)); +static cl::opt YamlDstDir( + "hyde-yaml-dir", + cl::desc("Root directory for YAML validation / update"), + cl::cat(MyToolCategory)); + +static cl::opt EmitJson( + "hyde-emit-json", + cl::desc("Output JSON emitted from operation"), + cl::cat(MyToolCategory), + cl::ValueDisallowed); + +static cl::opt TestedBy( + "hyde-tested-by", + cl::values( + clEnumValN(hyde::attribute_category::disabled, "disabled", "Disable tested_by attribute (default)"), + clEnumValN(hyde::attribute_category::required, "required", "Require tested_by attribute"), + clEnumValN(hyde::attribute_category::optional, "optional", "Enable tested_by attribute with optional value")), + cl::cat(MyToolCategory)); + static cl::opt YamlSrcDir( "hyde-src-root", cl::desc("The root path to the header file(s) being analyzed"), @@ -302,6 +319,11 @@ std::vector integrate_hyde_config(int argc, const char** argv) { hyde_flags.emplace_back("-hyde-yaml-dir=" + abs_path_str); } + if (config.count("hyde-tested-by")) { + const std::string& tested_by = config["hyde-tested-by"]; + hyde_flags.emplace_back("-hyde-tested-by=" + tested_by); + } + hyde_flags.insert(hyde_flags.end(), cli_hyde_flags.begin(), cli_hyde_flags.end()); clang_flags.insert(clang_flags.end(), cli_clang_flags.begin(), cli_clang_flags.end()); @@ -501,9 +523,15 @@ int main(int argc, const char** argv) try { filesystem::path src_root(YamlSrcDir); filesystem::path dst_root(YamlDstDir); - output_yaml(std::move(result), std::move(src_root), std::move(dst_root), + hyde::emit_options emit_options{ TestedBy }; + auto out_emitted = hyde::json::object(); + output_yaml(std::move(result), std::move(src_root), std::move(dst_root), out_emitted, ToolMode == ToolModeYAMLValidate ? hyde::yaml_mode::validate : - hyde::yaml_mode::update); + hyde::yaml_mode::update, std::move(emit_options)); + + if (EmitJson) { + std::cout << out_emitted << '\n'; + } } } catch (const std::exception& error) { std::cerr << "Error: " << error.what() << '\n'; diff --git a/sources/output_yaml.cpp b/sources/output_yaml.cpp index 8c8049b..7499f4c 100644 --- a/sources/output_yaml.cpp +++ b/sources/output_yaml.cpp @@ -39,36 +39,48 @@ namespace hyde { void output_yaml(json j, const boost::filesystem::path& src_root, const boost::filesystem::path& dst_root, - yaml_mode mode) { + json& out_emitted, + yaml_mode mode, + const emit_options& options) { bool failure{false}; + auto& library_emitted = out_emitted; // Process top-level library - yaml_library_emitter(src_root, dst_root, mode).emit(j); + yaml_library_emitter(src_root, dst_root, mode, options).emit(j, library_emitted); // Process sourcefile - yaml_sourcefile_emitter sourcefile_emitter(src_root, dst_root, mode); - sourcefile_emitter.emit(j); + yaml_sourcefile_emitter sourcefile_emitter(src_root, dst_root, mode, options); + auto sourcefile_emitted = hyde::json::object(); + sourcefile_emitter.emit(j, sourcefile_emitted); // Process classes - yaml_class_emitter class_emitter(src_root, dst_root, mode); + yaml_class_emitter class_emitter(src_root, dst_root, mode, options); for (const auto& c : j["classes"]) { - failure |= class_emitter.emit(c); + auto class_emitted = hyde::json::object(); + failure |= class_emitter.emit(c, class_emitted); + sourcefile_emitted["classes"].push_back(std::move(class_emitted)); } // Process enums - yaml_enum_emitter enum_emitter(src_root, dst_root, mode); + yaml_enum_emitter enum_emitter(src_root, dst_root, mode, options); for (const auto& c : j["enums"]) { - failure |= enum_emitter.emit(c); + auto enum_emitted = hyde::json::object(); + failure |= enum_emitter.emit(c, enum_emitted); + sourcefile_emitted["enums"].push_back(std::move(enum_emitted)); } // Process functions - yaml_function_emitter function_emitter(src_root, dst_root, mode, false); + yaml_function_emitter function_emitter(src_root, dst_root, mode, options, false); const auto& functions = j["functions"]; for (auto it = functions.begin(); it != functions.end(); ++it) { function_emitter.set_key(it.key()); - failure |= function_emitter.emit(it.value()); + auto function_emitted = hyde::json::object(); + failure |= function_emitter.emit(it.value(), function_emitted); + sourcefile_emitted["functions"].push_back(std::move(function_emitted)); } + library_emitted["sourcefiles"].push_back(std::move(sourcefile_emitted)); + // Check for extra files. Always do this last. failure |= sourcefile_emitter.extraneous_file_check();