Refactored comment parsing to clang comments

This commit is contained in:
Bartek Kryza
2022-09-18 23:57:02 +02:00
parent e45458de62
commit 920388d84a
31 changed files with 749 additions and 144 deletions

View File

@@ -133,10 +133,7 @@ inja::json generator<C, D>::element_context(const E &e) const
}
if (e.comment().has_value()) {
std::string c = e.comment().value();
if (!c.empty()) {
ctx["element"]["comment"] = util::trim(c);
}
ctx["element"]["comment"] = e.comment().value();
}
return ctx;
@@ -186,27 +183,35 @@ void generator<C, D>::generate_plantuml_directives(
using common::model::namespace_;
for (const auto &d : directives) {
// Render the directive with template engine first
std::string directive{env().render(std::string_view{d}, context())};
try {
// Render the directive with template engine first
std::string directive{env().render(std::string_view{d}, context())};
// Now search for alias @A() directives in the text
std::tuple<std::string, size_t, size_t> alias_match;
while (util::find_element_alias(directive, alias_match)) {
const auto full_name =
m_config.using_namespace() | std::get<0>(alias_match);
auto element_opt = m_model.get(full_name.to_string());
// Now search for alias @A() directives in the text
// (this is deprecated)
std::tuple<std::string, size_t, size_t> alias_match;
while (util::find_element_alias(directive, alias_match)) {
const auto full_name =
m_config.using_namespace() | std::get<0>(alias_match);
auto element_opt = m_model.get(full_name.to_string());
if (element_opt)
directive.replace(std::get<1>(alias_match),
std::get<2>(alias_match), element_opt.value().alias());
else {
LOG_ERROR("Cannot find clang-uml alias for element {}",
full_name.to_string());
directive.replace(std::get<1>(alias_match),
std::get<2>(alias_match), "UNKNOWN_ALIAS");
if (element_opt)
directive.replace(std::get<1>(alias_match),
std::get<2>(alias_match), element_opt.value().alias());
else {
LOG_ERROR("Cannot find clang-uml alias for element {}",
full_name.to_string());
directive.replace(std::get<1>(alias_match),
std::get<2>(alias_match), "UNKNOWN_ALIAS");
}
}
ostr << directive << '\n';
}
catch (const inja::json::exception &e) {
LOG_ERROR("Failed to render PlantUML directive: \n{}\n due to: {}",
d, e.what());
}
ostr << directive << '\n';
}
}
@@ -433,10 +438,14 @@ template <typename C, typename D> void generator<C, D>::init_env()
// {{ element("clanguml::t00050::A").comment }}
//
m_env.add_callback("element", 1, [this](inja::Arguments &args) {
inja::json res{};
auto element_opt = m_model.get_with_namespace(
args[0]->get<std::string>(), m_config.using_namespace());
return element_opt.value().context();
if (element_opt.has_value())
res = element_opt.value().context();
return res;
});
// Convert C++ entity to PlantUML alias, e.g.
@@ -457,15 +466,17 @@ template <typename C, typename D> void generator<C, D>::init_env()
// {{ element("A").comment }}
//
m_env.add_callback("comment", 1, [this](inja::Arguments &args) {
std::string res{};
inja::json res{};
auto element = m_model.get_with_namespace(
args[0]->get<std::string>(), m_config.using_namespace());
if (element.has_value()) {
auto comment = element.value().comment();
if (comment.has_value())
if (comment.has_value()) {
assert(comment.value().is_object());
res = comment.value();
}
}
return res;

View File

@@ -88,10 +88,7 @@ void decorated_element::append(const decorated_element &de)
}
}
std::optional<std::string> decorated_element::comment() const
{
return comment_;
}
std::optional<comment_t> decorated_element::comment() const { return comment_; }
void decorated_element::set_comment(const std::string &c) { comment_ = c; }
void decorated_element::set_comment(const comment_t &c) { comment_ = c; }
}

View File

@@ -20,14 +20,20 @@
#include "enums.h"
#include "decorators/decorators.h"
#include "inja/inja.hpp"
#include <any>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <variant>
#include <vector>
namespace clanguml::common::model {
using comment_t = inja::json;
class decorated_element {
public:
bool skip() const;
@@ -46,13 +52,13 @@ public:
void append(const decorated_element &de);
std::optional<std::string> comment() const;
std::optional<comment_t> comment() const;
void set_comment(const std::string &c);
void set_comment(const comment_t &c);
private:
std::vector<std::shared_ptr<decorators::decorator>> decorators_;
std::optional<std::string> comment_;
std::optional<comment_t> comment_;
};
}

View File

@@ -19,6 +19,7 @@
#include "decorated_element.h"
#include "relationship.h"
#include "source_location.h"
#include "util/util.h"
#include <inja/inja.hpp>
@@ -30,7 +31,7 @@
namespace clanguml::common::model {
class diagram_element : public decorated_element {
class diagram_element : public decorated_element, public source_location {
public:
using id_t = int64_t;

View File

@@ -44,8 +44,9 @@ inja::json element::context() const
ctx["alias"] = alias();
ctx["full_name"] = full_name(false);
ctx["namespace"] = get_namespace().to_string();
if (comment().has_value())
if (comment().has_value()) {
ctx["comment"] = comment().value();
}
return ctx;
}

View File

@@ -32,7 +32,7 @@
namespace clanguml::common::model {
class element : public diagram_element, public source_location {
class element : public diagram_element {
public:
element(const namespace_ &using_namespace);

View File

@@ -45,7 +45,6 @@ using filesystem_path = common::model::path<fs_path_sep>;
class source_file
: public common::model::diagram_element,
public common::model::stylable_element,
public source_location,
public common::model::nested_trait<common::model::source_file,
filesystem_path> {
public:

View File

@@ -0,0 +1,221 @@
/**
* src/common/visitor/comment/clang_visitor.cc
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "clang_visitor.h"
namespace clanguml::common::visitor::comment {
clang_visitor::clang_visitor(clang::SourceManager &source_manager)
: comment_visitor{source_manager}
{
}
void clang_visitor::visit(
const clang::NamedDecl &decl, common::model::decorated_element &e)
{
const auto *comment =
decl.getASTContext().getRawCommentForDeclNoCache(&decl);
if (comment == nullptr) {
return;
}
auto raw_comment = comment->getRawText(source_manager());
auto formatted_comment = comment->getFormattedText(
source_manager(), decl.getASTContext().getDiagnostics());
common::model::comment_t cmt = inja::json::object();
cmt["raw"] = raw_comment;
cmt["formatted"] = formatted_comment;
using clang::comments::BlockCommandComment;
using clang::comments::Comment;
using clang::comments::FullComment;
using clang::comments::ParagraphComment;
using clang::comments::ParamCommandComment;
using clang::comments::TextComment;
using clang::comments::TParamCommandComment;
FullComment *full_comment =
comment->parse(decl.getASTContext(), nullptr, &decl);
const auto &traits = decl.getASTContext().getCommentCommandTraits();
for (const auto *block : full_comment->getBlocks()) {
const auto block_kind = block->getCommentKind();
if (block_kind == Comment::ParagraphCommentKind) {
std::string paragraph_text;
visit_paragraph(clang::dyn_cast<ParagraphComment>(block), traits,
paragraph_text);
if (!cmt.contains("text"))
cmt["text"] = "";
cmt["text"] =
cmt["text"].get<std::string>() + "\n" + paragraph_text;
}
else if (block_kind == Comment::TextCommentKind) {
// TODO
}
else if (block_kind == Comment::ParamCommandCommentKind) {
visit_param_command(
clang::dyn_cast<ParamCommandComment>(block), traits, cmt);
}
else if (block_kind == Comment::TParamCommandCommentKind) {
visit_tparam_command(
clang::dyn_cast<TParamCommandComment>(block), traits, cmt);
}
else if (block_kind == Comment::BlockCommandCommentKind) {
auto *command = clang::dyn_cast<BlockCommandComment>(block);
auto command_info = traits.getCommandInfo(command->getCommandID());
if (command_info->IsBlockCommand && command_info->NumArgs == 0) {
// Visit block command with a single text argument, e.g.:
// \brief text
// \todo text
// ...
visit_block_command(command, traits, cmt);
}
else if (command_info->IsParamCommand) {
// Visit function param block:
// \param arg text
visit_param_command(
clang::dyn_cast<ParamCommandComment>(command), traits, cmt);
}
else if (command_info->IsTParamCommand) {
// Visit template param block:
// \tparam typename text
visit_tparam_command(
clang::dyn_cast<TParamCommandComment>(command), traits,
cmt);
}
}
}
e.set_comment(cmt);
}
void clang_visitor::visit_block_command(
const clang::comments::BlockCommandComment *command,
const clang::comments::CommandTraits &traits, common::model::comment_t &cmt)
{
using clang::comments::Comment;
using clang::comments::ParagraphComment;
using clang::comments::TextComment;
std::string command_text;
for (auto paragraph_it = command->child_begin();
paragraph_it != command->child_end(); ++paragraph_it) {
if ((*paragraph_it)->getCommentKind() ==
Comment::ParagraphCommentKind) {
visit_paragraph(clang::dyn_cast<ParagraphComment>(*paragraph_it),
traits, command_text);
}
}
const auto command_name = command->getCommandName(traits).str();
if (!command_text.empty()) {
if (!cmt.contains(command_name))
cmt[command_name] = inja::json::array();
cmt[command_name].push_back(command_text);
}
}
void clang_visitor::visit_param_command(
const clang::comments::ParamCommandComment *command,
const clang::comments::CommandTraits &traits, common::model::comment_t &cmt)
{
using clang::comments::Comment;
using clang::comments::ParagraphComment;
using clang::comments::TextComment;
std::string description;
const auto name = command->getParamNameAsWritten().str();
for (auto it = command->child_begin(); it != command->child_end(); ++it) {
if ((*it)->getCommentKind() == Comment::ParagraphCommentKind) {
visit_paragraph(
clang::dyn_cast<ParagraphComment>(*it), traits, description);
}
}
if (!name.empty()) {
if (!cmt.contains("param"))
cmt["param"] = inja::json::array();
inja::json param = inja::json::object();
param["name"] = name;
param["description"] = description;
cmt["param"].push_back(std::move(param));
}
}
void clang_visitor::visit_tparam_command(
const clang::comments::TParamCommandComment *command,
const clang::comments::CommandTraits &traits, common::model::comment_t &cmt)
{
using clang::comments::Comment;
using clang::comments::ParagraphComment;
using clang::comments::TextComment;
std::string description;
const auto name = command->getParamNameAsWritten().str();
for (auto it = command->child_begin(); it != command->child_end(); ++it) {
if ((*it)->getCommentKind() == Comment::ParagraphCommentKind) {
visit_paragraph(
clang::dyn_cast<ParagraphComment>(*it), traits, description);
}
}
if (!name.empty()) {
if (!cmt.contains("tparam"))
cmt["tparam"] = inja::json::array();
inja::json param = inja::json::object();
param["name"] = name;
param["description"] = description;
cmt["tparam"].push_back(std::move(param));
}
}
void clang_visitor::visit_paragraph(
const clang::comments::ParagraphComment *paragraph,
const clang::comments::CommandTraits &traits, std::string &text)
{
using clang::comments::Comment;
using clang::comments::TextComment;
for (auto text_it = paragraph->child_begin();
text_it != paragraph->child_end(); ++text_it) {
if ((*text_it)->getCommentKind() == Comment::TextCommentKind) {
// Merge paragraph lines into a single string
text += clang::dyn_cast<TextComment>((*text_it))->getText();
text += "\n";
}
}
}
}

View File

@@ -0,0 +1,54 @@
/**
* src/common/visitor/comment/clang_visitor.h
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <clang/AST/ASTContext.h>
#include <clang/AST/Comment.h>
#include <clang/Basic/SourceManager.h>
#include "comment_visitor.h"
namespace clanguml::common::visitor::comment {
class clang_visitor : public comment_visitor {
public:
clang_visitor(clang::SourceManager &source_manager);
void visit(const clang::NamedDecl &decl,
common::model::decorated_element &e) override;
private:
void visit_block_command(
const clang::comments::BlockCommandComment *command,
const clang::comments::CommandTraits &traits,
common::model::comment_t &cmt);
void visit_param_command(
const clang::comments::ParamCommandComment *command,
const clang::comments::CommandTraits &traits,
common::model::comment_t &cmt);
void visit_tparam_command(
const clang::comments::TParamCommandComment *command,
const clang::comments::CommandTraits &traits,
common::model::comment_t &cmt);
void visit_paragraph(const clang::comments::ParagraphComment *paragraph,
const clang::comments::CommandTraits &traits, std::string &text);
};
}

View File

@@ -0,0 +1,33 @@
/**
* src/common/visitor/comment/comment_visitor.cc
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "comment_visitor.h"
namespace clanguml::common::visitor::comment {
comment_visitor::comment_visitor(clang::SourceManager &source_manager)
: source_manager_{source_manager}
{
}
clang::SourceManager &comment_visitor::source_manager()
{
return source_manager_;
}
}

View File

@@ -0,0 +1,41 @@
/**
* src/common/visitor/comment/comment_visitor.h
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <clang/AST/Comment.h>
#include <clang/Basic/SourceManager.h>
#include "common/model/decorated_element.h"
namespace clanguml::common::visitor::comment {
class comment_visitor {
public:
comment_visitor(clang::SourceManager &source_manager);
virtual ~comment_visitor() = default;
virtual void visit(
const clang::NamedDecl &decl, common::model::decorated_element &e) = 0;
clang::SourceManager &source_manager();
private:
clang::SourceManager &source_manager_;
};
}

View File

@@ -0,0 +1,50 @@
/**
* src/common/visitor/comment/plain_visitor.cc
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "plain_visitor.h"
namespace clanguml::common::visitor::comment {
plain_visitor::plain_visitor(clang::SourceManager &source_manager)
: comment_visitor{source_manager}
{
}
void plain_visitor::visit(
const clang::NamedDecl &decl, common::model::decorated_element &e)
{
const auto *comment =
decl.getASTContext().getRawCommentForDeclNoCache(&decl);
if (comment == nullptr) {
return;
}
auto raw_comment = comment->getRawText(source_manager());
auto formatted_comment = comment->getFormattedText(
source_manager(), decl.getASTContext().getDiagnostics());
common::model::comment_t cmt = inja::json::object();
cmt["raw"] = raw_comment;
cmt["formatted"] = formatted_comment;
e.set_comment(cmt);
}
}

View File

@@ -0,0 +1,35 @@
/**
* src/common/visitor/comment/plain_visitor.h
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <clang/AST/ASTContext.h>
#include <clang/AST/Comment.h>
#include <clang/Basic/SourceManager.h>
#include "comment_visitor.h"
namespace clanguml::common::visitor::comment {
class plain_visitor : public comment_visitor {
public:
plain_visitor(clang::SourceManager &source_manager);
void visit(const clang::NamedDecl &decl,
common::model::decorated_element &e) override;
};
}

View File

@@ -0,0 +1,76 @@
/**
* src/common/visitor/translation_unit_visitor.cc
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "translation_unit_visitor.h"
#include "comment/clang_visitor.h"
#include "comment/plain_visitor.h"
namespace clanguml::common::visitor {
translation_unit_visitor::translation_unit_visitor(
clang::SourceManager &sm, const clanguml::config::diagram &config)
: source_manager_{sm}
, config_{config}
{
if (config.comment_parser() == config::comment_parser_t::plain) {
comment_visitor_ =
std::make_unique<comment::plain_visitor>(source_manager_);
}
else if (config.comment_parser() == config::comment_parser_t::clang) {
comment_visitor_ =
std::make_unique<comment::clang_visitor>(source_manager_);
}
}
clang::SourceManager &translation_unit_visitor::source_manager() const
{
return source_manager_;
}
void translation_unit_visitor::process_comment(
const clang::NamedDecl &decl, clanguml::common::model::decorated_element &e)
{
assert(comment_visitor_.get() != nullptr);
comment_visitor_->visit(decl, e);
const auto *comment =
decl.getASTContext().getRawCommentForDeclNoCache(&decl);
if (comment != nullptr) {
// Process clang-uml decorators in the comments
// TODO: Refactor to use standard block comments processable by clang
// comments
e.add_decorators(decorators::parse(comment->getFormattedText(
source_manager_, decl.getASTContext().getDiagnostics())));
}
}
void translation_unit_visitor::set_source_location(
const clang::Decl &decl, clanguml::common::model::source_location &element)
{
if (decl.getLocation().isValid()) {
element.set_file(source_manager_.getFilename(decl.getLocation()).str());
element.set_line(
source_manager_.getSpellingLineNumber(decl.getLocation()));
}
}
}

View File

@@ -0,0 +1,62 @@
/**
* src/common/visitor/translation_unit_visitor.h
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "comment/comment_visitor.h"
#include "config/config.h"
#include <clang/AST/Comment.h>
#include <clang/Basic/SourceManager.h>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <string>
namespace clanguml::common::visitor {
using found_relationships_t =
std::vector<std::pair<clanguml::common::model::diagram_element::id_t,
common::model::relationship_t>>;
class translation_unit_visitor {
public:
explicit translation_unit_visitor(
clang::SourceManager &sm, const clanguml::config::diagram &config);
clang::SourceManager &source_manager() const;
void finalize();
protected:
void set_source_location(const clang::Decl &decl,
clanguml::common::model::source_location &element);
void process_comment(const clang::NamedDecl &decl,
clanguml::common::model::decorated_element &e);
private:
clang::SourceManager &source_manager_;
// Reference to diagram config
const clanguml::config::diagram &config_;
std::unique_ptr<comment::comment_visitor> comment_visitor_;
};
}