Added initial support for include graph diagrams
This commit is contained in:
@@ -27,6 +27,8 @@ Main features supported so far include:
|
|||||||
* **Package diagram generation**
|
* **Package diagram generation**
|
||||||
* Generation of package diagram based on C++ namespaces
|
* Generation of package diagram based on C++ namespaces
|
||||||
* Interactive links to online code to packages
|
* Interactive links to online code to packages
|
||||||
|
* **Include graph diagram generation**
|
||||||
|
* Show include graph for selected files
|
||||||
|
|
||||||
To see what `clang-uml` can do so far, checkout the diagrams generated for unit test cases [here](./docs/test_cases.md).
|
To see what `clang-uml` can do so far, checkout the diagrams generated for unit test cases [here](./docs/test_cases.md).
|
||||||
|
|
||||||
|
|||||||
@@ -85,4 +85,12 @@ bool diagram::should_include(
|
|||||||
return filter_->should_include(ns, name);
|
return filter_->should_include(ns, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool diagram::should_include(const common::model::source_file &f) const
|
||||||
|
{
|
||||||
|
if (filter_.get() == nullptr)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return filter_->should_include(f);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "diagram_element.h"
|
#include "diagram_element.h"
|
||||||
#include "enums.h"
|
#include "enums.h"
|
||||||
#include "namespace.h"
|
#include "namespace.h"
|
||||||
|
#include "source_file.h"
|
||||||
|
|
||||||
#include <type_safe/optional_ref.hpp>
|
#include <type_safe/optional_ref.hpp>
|
||||||
|
|
||||||
@@ -58,9 +59,11 @@ public:
|
|||||||
// TODO: refactor to a template method
|
// TODO: refactor to a template method
|
||||||
bool should_include(const element &e) const;
|
bool should_include(const element &e) const;
|
||||||
bool should_include(const std::string &e) const;
|
bool should_include(const std::string &e) const;
|
||||||
|
bool should_include(const source_file &path) const;
|
||||||
bool should_include(const relationship r) const;
|
bool should_include(const relationship r) const;
|
||||||
bool should_include(const relationship_t r) const;
|
bool should_include(const relationship_t r) const;
|
||||||
bool should_include(const access_t s) const;
|
bool should_include(const access_t s) const;
|
||||||
|
|
||||||
bool should_include(const namespace_ &ns, const std::string &name) const;
|
bool should_include(const namespace_ &ns, const std::string &name) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ tvl::value_t filter_visitor::match(
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tvl::value_t filter_visitor::match(
|
||||||
|
const diagram &d, const common::model::source_file &f) const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
bool filter_visitor::is_inclusive() const
|
bool filter_visitor::is_inclusive() const
|
||||||
{
|
{
|
||||||
return type_ == filter_t::kInclusive;
|
return type_ == filter_t::kInclusive;
|
||||||
@@ -254,6 +260,40 @@ tvl::value_t context_filter::match(const diagram &d, const element &e) const
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paths_filter::paths_filter(filter_t type, const std::filesystem::path &root,
|
||||||
|
std::vector<std::filesystem::path> p)
|
||||||
|
: filter_visitor{type}
|
||||||
|
, root_{root}
|
||||||
|
{
|
||||||
|
for (auto &&path : p) {
|
||||||
|
std::filesystem::path absolute_path;
|
||||||
|
|
||||||
|
if (path.is_relative())
|
||||||
|
absolute_path = root / path;
|
||||||
|
|
||||||
|
absolute_path = absolute_path.lexically_normal();
|
||||||
|
|
||||||
|
paths_.emplace_back(std::move(absolute_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tvl::value_t paths_filter::match(
|
||||||
|
const diagram &d, const common::model::source_file &p) const
|
||||||
|
{
|
||||||
|
|
||||||
|
if (paths_.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pp = p.fs_path(root_);
|
||||||
|
for (const auto &path : paths_) {
|
||||||
|
if (util::starts_with(pp, path))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
diagram_filter::diagram_filter(
|
diagram_filter::diagram_filter(
|
||||||
const common::model::diagram &d, const config::diagram &c)
|
const common::model::diagram &d, const config::diagram &c)
|
||||||
: diagram_{d}
|
: diagram_{d}
|
||||||
@@ -295,6 +335,8 @@ void diagram_filter::init_filters(const config::diagram &c)
|
|||||||
filter_t::kInclusive, c.include().relationships));
|
filter_t::kInclusive, c.include().relationships));
|
||||||
inclusive_.emplace_back(std::make_unique<access_filter>(
|
inclusive_.emplace_back(std::make_unique<access_filter>(
|
||||||
filter_t::kInclusive, c.include().access));
|
filter_t::kInclusive, c.include().access));
|
||||||
|
inclusive_.emplace_back(std::make_unique<paths_filter>(
|
||||||
|
filter_t::kInclusive, c.base_directory(), c.include().paths));
|
||||||
|
|
||||||
// Include any of these matches even if one them does not match
|
// Include any of these matches even if one them does not match
|
||||||
std::vector<std::unique_ptr<filter_visitor>> element_filters;
|
std::vector<std::unique_ptr<filter_visitor>> element_filters;
|
||||||
@@ -305,6 +347,7 @@ void diagram_filter::init_filters(const config::diagram &c)
|
|||||||
element_filters.emplace_back(std::make_unique<context_filter>(
|
element_filters.emplace_back(std::make_unique<context_filter>(
|
||||||
filter_t::kInclusive, c.include().context));
|
filter_t::kInclusive, c.include().context));
|
||||||
|
|
||||||
|
|
||||||
inclusive_.emplace_back(std::make_unique<anyof_filter>(
|
inclusive_.emplace_back(std::make_unique<anyof_filter>(
|
||||||
filter_t::kInclusive, std::move(element_filters)));
|
filter_t::kInclusive, std::move(element_filters)));
|
||||||
}
|
}
|
||||||
@@ -323,6 +366,8 @@ void diagram_filter::init_filters(const config::diagram &c)
|
|||||||
filter_t::kExclusive, c.exclude().subclasses));
|
filter_t::kExclusive, c.exclude().subclasses));
|
||||||
exclusive_.emplace_back(std::make_unique<context_filter>(
|
exclusive_.emplace_back(std::make_unique<context_filter>(
|
||||||
filter_t::kExclusive, c.exclude().context));
|
filter_t::kExclusive, c.exclude().context));
|
||||||
|
exclusive_.emplace_back(std::make_unique<paths_filter>(
|
||||||
|
filter_t::kInclusive, c.base_directory(), c.exclude().paths));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,11 @@
|
|||||||
#include "config/config.h"
|
#include "config/config.h"
|
||||||
#include "cx/util.h"
|
#include "cx/util.h"
|
||||||
#include "diagram.h"
|
#include "diagram.h"
|
||||||
|
#include "source_file.h"
|
||||||
#include "tvl.h"
|
#include "tvl.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
namespace clanguml::common::model {
|
namespace clanguml::common::model {
|
||||||
|
|
||||||
enum filter_t { kInclusive, kExclusive };
|
enum filter_t { kInclusive, kExclusive };
|
||||||
@@ -47,6 +50,9 @@ public:
|
|||||||
virtual tvl::value_t match(
|
virtual tvl::value_t match(
|
||||||
const diagram &d, const common::model::namespace_ &ns) const;
|
const diagram &d, const common::model::namespace_ &ns) const;
|
||||||
|
|
||||||
|
virtual tvl::value_t match(
|
||||||
|
const diagram &d, const common::model::source_file &f) const;
|
||||||
|
|
||||||
bool is_inclusive() const;
|
bool is_inclusive() const;
|
||||||
bool is_exclusive() const;
|
bool is_exclusive() const;
|
||||||
|
|
||||||
@@ -125,6 +131,18 @@ private:
|
|||||||
std::vector<std::string> context_;
|
std::vector<std::string> context_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct paths_filter : public filter_visitor {
|
||||||
|
paths_filter(filter_t type, const std::filesystem::path &root,
|
||||||
|
std::vector<std::filesystem::path> p);
|
||||||
|
|
||||||
|
tvl::value_t match(const diagram &d,
|
||||||
|
const common::model::source_file &r) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::filesystem::path> paths_;
|
||||||
|
std::filesystem::path root_;
|
||||||
|
};
|
||||||
|
|
||||||
class diagram_filter {
|
class diagram_filter {
|
||||||
public:
|
public:
|
||||||
diagram_filter(const common::model::diagram &d, const config::diagram &c);
|
diagram_filter(const common::model::diagram &d, const config::diagram &c);
|
||||||
@@ -144,7 +162,9 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
auto inc = tvl::all_of(inclusive_.begin(), inclusive_.end(),
|
auto inc = tvl::all_of(inclusive_.begin(), inclusive_.end(),
|
||||||
[this, &e](const auto &in) { return in->match(diagram_, e); });
|
[this, &e](const auto &in) {
|
||||||
|
return in->match(diagram_, e);
|
||||||
|
});
|
||||||
|
|
||||||
if (tvl::is_undefined(inc) || tvl::is_true(inc))
|
if (tvl::is_undefined(inc) || tvl::is_true(inc))
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename V = T>
|
template <typename V = T>
|
||||||
void add_element(const Path &path, std::unique_ptr<V> p)
|
void add_element(
|
||||||
|
const Path &path, std::unique_ptr<V> p)
|
||||||
{
|
{
|
||||||
assert(p);
|
assert(p);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* src/include_diagram/model/source_file.cc
|
* src/common/model/source_file.cc
|
||||||
*
|
*
|
||||||
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
|
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
|
||||||
*
|
*
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* src/include_diagram/model/source_file.h
|
* src/common/model/source_file.h
|
||||||
*
|
*
|
||||||
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
|
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
|
||||||
*
|
*
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace clanguml::include_diagram::model {
|
namespace clanguml::common::model {
|
||||||
|
|
||||||
enum class source_file_type { kDirectory, kHeader, kImplementation };
|
enum class source_file_t { kDirectory, kHeader, kImplementation };
|
||||||
|
|
||||||
struct fs_path_sep {
|
struct fs_path_sep {
|
||||||
static constexpr std::string_view value = "/";
|
static constexpr std::string_view value = "/";
|
||||||
@@ -43,13 +43,19 @@ using filesystem_path = common::model::path<fs_path_sep>;
|
|||||||
class source_file
|
class source_file
|
||||||
: public common::model::diagram_element,
|
: public common::model::diagram_element,
|
||||||
public common::model::stylable_element,
|
public common::model::stylable_element,
|
||||||
public common::model::nested_trait<common::model::diagram_element,
|
public common::model::nested_trait<common::model::source_file,
|
||||||
filesystem_path> {
|
filesystem_path> {
|
||||||
public:
|
public:
|
||||||
source_file() = default;
|
source_file() = default;
|
||||||
|
|
||||||
void set_path(const filesystem_path &p) { path_ = p; }
|
void set_path(const filesystem_path &p) { path_ = p; }
|
||||||
|
|
||||||
|
void set_absolute() { is_absolute_ = true; }
|
||||||
|
|
||||||
|
void set_type(source_file_t type) { type_ = type; }
|
||||||
|
|
||||||
|
source_file_t type() const { return type_; }
|
||||||
|
|
||||||
source_file(const source_file &) = delete;
|
source_file(const source_file &) = delete;
|
||||||
source_file(source_file &&) = default;
|
source_file(source_file &&) = default;
|
||||||
source_file &operator=(const source_file &) = delete;
|
source_file &operator=(const source_file &) = delete;
|
||||||
@@ -62,23 +68,41 @@ public:
|
|||||||
return (path_ | name()).to_string();
|
return (path_ | name()).to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
void add_file(std::unique_ptr<include_diagram::model::source_file> &&f)
|
void add_file(std::unique_ptr<source_file> &&f)
|
||||||
{
|
{
|
||||||
LOG_DBG("Adding source file: {}, {}", f->name(), f->full_name(true));
|
LOG_DBG("Adding source file: {}, {}", f->name(), f->full_name(true));
|
||||||
|
|
||||||
add_element(f->path(), std::move(f));
|
add_element(f->path(), std::move(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::filesystem::path fs_path(const std::filesystem::path &base = {}) const
|
||||||
|
{
|
||||||
|
std::filesystem::path res;
|
||||||
|
|
||||||
|
for (const auto &pe : path_) {
|
||||||
|
res /= pe;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_absolute_)
|
||||||
|
res = "/" / res;
|
||||||
|
else
|
||||||
|
res = base / res;
|
||||||
|
|
||||||
|
return res.lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
filesystem_path path_;
|
filesystem_path path_;
|
||||||
|
source_file_t type_{source_file_t::kDirectory};
|
||||||
|
bool is_absolute_{false};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace std {
|
namespace std {
|
||||||
|
|
||||||
template <> struct hash<clanguml::include_diagram::model::filesystem_path> {
|
template <> struct hash<clanguml::common::model::filesystem_path> {
|
||||||
std::size_t operator()(
|
std::size_t operator()(
|
||||||
const clanguml::include_diagram::model::filesystem_path &key) const
|
const clanguml::common::model::filesystem_path &key) const
|
||||||
{
|
{
|
||||||
using clanguml::common::model::path;
|
using clanguml::common::model::path;
|
||||||
|
|
||||||
@@ -15,7 +15,9 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
namespace clanguml {
|
namespace clanguml {
|
||||||
@@ -28,12 +30,13 @@ config load(const std::string &config_file)
|
|||||||
|
|
||||||
// Store the parent path of the config_file to properly resolve
|
// Store the parent path of the config_file to properly resolve
|
||||||
// the include files paths
|
// the include files paths
|
||||||
auto config_file_path = std::filesystem::path{config_file};
|
auto config_file_path =
|
||||||
|
std::filesystem::absolute(std::filesystem::path{config_file});
|
||||||
doc.force_insert(
|
doc.force_insert(
|
||||||
"__parent_path", config_file_path.parent_path().string());
|
"__parent_path", config_file_path.parent_path().string());
|
||||||
|
|
||||||
// If the current directory is also a git repository,
|
// If the current directory is also a git repository,
|
||||||
// load some config values which can be included in the
|
// load some config values, which can be included in the
|
||||||
// generated diagrams
|
// generated diagrams
|
||||||
if (util::is_git_repository() && !doc["git"]) {
|
if (util::is_git_repository() && !doc["git"]) {
|
||||||
YAML::Node git_config{YAML::NodeType::Map};
|
YAML::Node git_config{YAML::NodeType::Map};
|
||||||
@@ -108,6 +111,8 @@ void inheritable_diagram_options::inherit(
|
|||||||
generate_method_arguments.override(parent.generate_method_arguments);
|
generate_method_arguments.override(parent.generate_method_arguments);
|
||||||
generate_links.override(parent.generate_links);
|
generate_links.override(parent.generate_links);
|
||||||
git.override(parent.git);
|
git.override(parent.git);
|
||||||
|
base_directory.override(parent.base_directory);
|
||||||
|
relative_to.override(parent.relative_to);
|
||||||
}
|
}
|
||||||
|
|
||||||
diagram_type class_diagram::type() const { return diagram_type::class_diagram; }
|
diagram_type class_diagram::type() const { return diagram_type::class_diagram; }
|
||||||
@@ -245,6 +250,21 @@ std::shared_ptr<clanguml::config::diagram> parse_diagram_config(const Node &d)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// config std::filesystem::path decoder
|
||||||
|
//
|
||||||
|
template <> struct convert<std::filesystem::path> {
|
||||||
|
static bool decode(const Node &node, std::filesystem::path &rhs)
|
||||||
|
{
|
||||||
|
if (!node.IsScalar())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
rhs = std::filesystem::path{node.as<std::string>()};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// config access_t decoder
|
// config access_t decoder
|
||||||
//
|
//
|
||||||
@@ -391,6 +411,9 @@ template <> struct convert<filter> {
|
|||||||
if (node["context"])
|
if (node["context"])
|
||||||
rhs.context = node["context"].as<decltype(rhs.context)>();
|
rhs.context = node["context"].as<decltype(rhs.context)>();
|
||||||
|
|
||||||
|
if (node["paths"])
|
||||||
|
rhs.paths = node["paths"].as<decltype(rhs.paths)>();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -505,6 +528,16 @@ template <> struct convert<include_diagram> {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
get_option(node, rhs.layout);
|
get_option(node, rhs.layout);
|
||||||
|
get_option(node, rhs.relative_to);
|
||||||
|
|
||||||
|
// Convert the path in relative_to to an absolute path, with respect
|
||||||
|
// to the directory where the `.clang-uml` configuration file is located
|
||||||
|
if (rhs.relative_to) {
|
||||||
|
auto absolute_relative_to =
|
||||||
|
std::filesystem::path{node["__parent_path"].as<std::string>()} /
|
||||||
|
rhs.relative_to();
|
||||||
|
rhs.relative_to.set(absolute_relative_to.lexically_normal());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -558,25 +591,29 @@ template <> struct convert<config> {
|
|||||||
get_option(node, rhs.generate_packages);
|
get_option(node, rhs.generate_packages);
|
||||||
get_option(node, rhs.generate_links);
|
get_option(node, rhs.generate_links);
|
||||||
get_option(node, rhs.git);
|
get_option(node, rhs.git);
|
||||||
|
rhs.base_directory.set(node["__parent_path"].as<std::string>());
|
||||||
|
get_option(node, rhs.relative_to);
|
||||||
|
|
||||||
auto diagrams = node["diagrams"];
|
auto diagrams = node["diagrams"];
|
||||||
|
|
||||||
assert(diagrams.Type() == NodeType::Map);
|
assert(diagrams.Type() == NodeType::Map);
|
||||||
|
|
||||||
for (const auto &d : diagrams) {
|
for (auto d : diagrams) {
|
||||||
auto name = d.first.as<std::string>();
|
auto name = d.first.as<std::string>();
|
||||||
std::shared_ptr<clanguml::config::diagram> diagram_config{};
|
std::shared_ptr<clanguml::config::diagram> diagram_config{};
|
||||||
|
auto parent_path = node["__parent_path"].as<std::string>();
|
||||||
|
|
||||||
if (has_key(d.second, "include!")) {
|
if (has_key(d.second, "include!")) {
|
||||||
auto parent_path = node["__parent_path"].as<std::string>();
|
|
||||||
auto include_path = std::filesystem::path{parent_path};
|
auto include_path = std::filesystem::path{parent_path};
|
||||||
include_path /= d.second["include!"].as<std::string>();
|
include_path /= d.second["include!"].as<std::string>();
|
||||||
|
|
||||||
YAML::Node node = YAML::LoadFile(include_path.string());
|
YAML::Node node = YAML::LoadFile(include_path.string());
|
||||||
|
node.force_insert("__parent_path", parent_path);
|
||||||
|
|
||||||
diagram_config = parse_diagram_config(node);
|
diagram_config = parse_diagram_config(node);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
d.second.force_insert("__parent_path", parent_path);
|
||||||
diagram_config = parse_diagram_config(d.second);
|
diagram_config = parse_diagram_config(d.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,6 +622,9 @@ template <> struct convert<config> {
|
|||||||
diagram_config->inherit(rhs);
|
diagram_config->inherit(rhs);
|
||||||
rhs.diagrams[name] = diagram_config;
|
rhs.diagrams[name] = diagram_config;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ struct filter {
|
|||||||
std::vector<std::string> subclasses;
|
std::vector<std::string> subclasses;
|
||||||
|
|
||||||
std::vector<std::string> context;
|
std::vector<std::string> context;
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> paths;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class hint_t { up, down, left, right };
|
enum class hint_t { up, down, left, right };
|
||||||
@@ -110,6 +112,8 @@ struct inheritable_diagram_options {
|
|||||||
option<bool> generate_packages{"generate_packages", false};
|
option<bool> generate_packages{"generate_packages", false};
|
||||||
option<generate_links_config> generate_links{"generate_links"};
|
option<generate_links_config> generate_links{"generate_links"};
|
||||||
option<git_config> git{"git"};
|
option<git_config> git{"git"};
|
||||||
|
option<std::filesystem::path> base_directory{"__parent_path"};
|
||||||
|
option<std::filesystem::path> relative_to{"relative_to"};
|
||||||
|
|
||||||
void inherit(const inheritable_diagram_options &parent);
|
void inherit(const inheritable_diagram_options &parent);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,11 +31,41 @@ void generator::generate_relationships(
|
|||||||
const source_file &f, std::ostream &ostr) const
|
const source_file &f, std::ostream &ostr) const
|
||||||
{
|
{
|
||||||
LOG_DBG("Generating relationships for file {}", f.full_name(true));
|
LOG_DBG("Generating relationships for file {}", f.full_name(true));
|
||||||
|
|
||||||
|
namespace plantuml_common = clanguml::common::generators::plantuml;
|
||||||
|
|
||||||
|
if (f.type() == common::model::source_file_t::kDirectory) {
|
||||||
|
for (const auto &file : f) {
|
||||||
|
generate_relationships(
|
||||||
|
dynamic_cast<const source_file &>(*file), ostr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const auto &r : f.relationships()) {
|
||||||
|
if (m_model.should_include(r.type())) {
|
||||||
|
ostr << f.alias() << " "
|
||||||
|
<< plantuml_common::to_plantuml(r.type(), r.style()) << " "
|
||||||
|
<< r.destination() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void generator::generate(const source_file &f, std::ostream &ostr) const
|
void generator::generate(const source_file &f, std::ostream &ostr) const
|
||||||
{
|
{
|
||||||
LOG_DBG("Generating source_file {}", f.name());
|
LOG_DBG("Generating source_file {}", f.name());
|
||||||
|
|
||||||
|
if (f.type() == common::model::source_file_t::kDirectory) {
|
||||||
|
ostr << "folder " << f.name();
|
||||||
|
ostr << " as " << f.alias() << " {\n";
|
||||||
|
for (const auto &file : f) {
|
||||||
|
generate(dynamic_cast<const source_file &>(*file), ostr);
|
||||||
|
}
|
||||||
|
ostr << "}" << '\n';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ostr << "file " << f.name() << " as " << f.alias() << '\n';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void generator::generate(std::ostream &ostr) const
|
void generator::generate(std::ostream &ostr) const
|
||||||
@@ -44,15 +74,14 @@ void generator::generate(std::ostream &ostr) const
|
|||||||
|
|
||||||
generate_plantuml_directives(ostr, m_config.puml().before);
|
generate_plantuml_directives(ostr, m_config.puml().before);
|
||||||
|
|
||||||
|
// Generate files and folders
|
||||||
for (const auto &p : m_model) {
|
for (const auto &p : m_model) {
|
||||||
generate(dynamic_cast<source_file &>(*p), ostr);
|
generate(dynamic_cast<source_file &>(*p), ostr);
|
||||||
ostr << '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process package relationships
|
// Process file include relationships
|
||||||
for (const auto &p : m_model) {
|
for (const auto &p : m_model) {
|
||||||
generate_relationships(dynamic_cast<source_file &>(*p), ostr);
|
generate_relationships(dynamic_cast<source_file &>(*p), ostr);
|
||||||
ostr << '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_config_layout_hints(ostr);
|
generate_config_layout_hints(ostr);
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
#include "common/generators/plantuml/generator.h"
|
#include "common/generators/plantuml/generator.h"
|
||||||
#include "common/model/package.h"
|
#include "common/model/package.h"
|
||||||
#include "common/model/relationship.h"
|
#include "common/model/relationship.h"
|
||||||
|
#include "common/model/source_file.h"
|
||||||
#include "config/config.h"
|
#include "config/config.h"
|
||||||
#include "cx/compilation_database.h"
|
#include "cx/compilation_database.h"
|
||||||
#include "include_diagram/model/diagram.h"
|
#include "include_diagram/model/diagram.h"
|
||||||
#include "include_diagram/model/source_file.h"
|
|
||||||
#include "include_diagram/visitor/translation_unit_visitor.h"
|
#include "include_diagram/visitor/translation_unit_visitor.h"
|
||||||
#include "util/util.h"
|
#include "util/util.h"
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ using common_generator =
|
|||||||
using clanguml::common::model::access_t;
|
using clanguml::common::model::access_t;
|
||||||
using clanguml::common::model::package;
|
using clanguml::common::model::package;
|
||||||
using clanguml::common::model::relationship_t;
|
using clanguml::common::model::relationship_t;
|
||||||
using clanguml::include_diagram::model::source_file;
|
using clanguml::common::model::source_file;
|
||||||
using namespace clanguml::util;
|
using namespace clanguml::util;
|
||||||
|
|
||||||
class generator : public common_generator<diagram_config, diagram_model> {
|
class generator : public common_generator<diagram_config, diagram_model> {
|
||||||
|
|||||||
@@ -34,16 +34,18 @@ type_safe::optional_ref<const common::model::diagram_element> diagram::get(
|
|||||||
return get_file(full_name);
|
return get_file(full_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
void diagram::add_file(std::unique_ptr<include_diagram::model::source_file> &&f)
|
void diagram::add_file(std::unique_ptr<common::model::source_file> &&f)
|
||||||
{
|
{
|
||||||
LOG_DBG("Adding source file: {}, {}", f->name(), f->full_name(true));
|
LOG_DBG("Adding source file: {}, {}", f->name(), f->full_name(true));
|
||||||
|
|
||||||
files_.emplace_back(*f);
|
files_.emplace_back(*f);
|
||||||
|
|
||||||
add_element(f->path(), std::move(f));
|
auto p = f->path();
|
||||||
|
|
||||||
|
add_element(p, std::move(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
type_safe::optional_ref<const include_diagram::model::source_file>
|
type_safe::optional_ref<const common::model::source_file>
|
||||||
diagram::get_file(const std::string &name) const
|
diagram::get_file(const std::string &name) const
|
||||||
{
|
{
|
||||||
for (const auto &p : files_) {
|
for (const auto &p : files_) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#include "common/model/diagram.h"
|
#include "common/model/diagram.h"
|
||||||
#include "common/model/package.h"
|
#include "common/model/package.h"
|
||||||
#include "source_file.h"
|
#include "common/model/source_file.h"
|
||||||
|
|
||||||
#include <type_safe/optional_ref.hpp>
|
#include <type_safe/optional_ref.hpp>
|
||||||
|
|
||||||
@@ -29,8 +29,9 @@
|
|||||||
namespace clanguml::include_diagram::model {
|
namespace clanguml::include_diagram::model {
|
||||||
|
|
||||||
class diagram : public clanguml::common::model::diagram,
|
class diagram : public clanguml::common::model::diagram,
|
||||||
public clanguml::common::model::nested_trait<source_file,
|
public clanguml::common::model::nested_trait<
|
||||||
filesystem_path> {
|
clanguml::common::model::source_file,
|
||||||
|
clanguml::common::model::filesystem_path> {
|
||||||
public:
|
public:
|
||||||
diagram() = default;
|
diagram() = default;
|
||||||
|
|
||||||
@@ -44,16 +45,15 @@ public:
|
|||||||
type_safe::optional_ref<const common::model::diagram_element> get(
|
type_safe::optional_ref<const common::model::diagram_element> get(
|
||||||
const std::string &full_name) const;
|
const std::string &full_name) const;
|
||||||
|
|
||||||
void add_file(std::unique_ptr<include_diagram::model::source_file> &&f);
|
void add_file(std::unique_ptr<common::model::source_file> &&f);
|
||||||
|
|
||||||
type_safe::optional_ref<const include_diagram::model::source_file> get_file(
|
type_safe::optional_ref<const common::model::source_file> get_file(
|
||||||
const std::string &name) const;
|
const std::string &name) const;
|
||||||
|
|
||||||
std::string to_alias(const std::string &full_name) const;
|
std::string to_alias(const std::string &full_name) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<
|
std::vector<type_safe::object_ref<const common::model::source_file, false>>
|
||||||
type_safe::object_ref<const include_diagram::model::source_file, false>>
|
|
||||||
files_;
|
files_;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ clanguml::include_diagram::model::diagram &translation_unit_context::diagram()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void translation_unit_context::set_current_file(
|
void translation_unit_context::set_current_file(
|
||||||
type_safe::optional_ref<include_diagram::model::source_file> f)
|
type_safe::optional_ref<common::model::source_file> f)
|
||||||
{
|
{
|
||||||
current_file_ = f;
|
current_file_ = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
type_safe::optional_ref<include_diagram::model::source_file>
|
type_safe::optional_ref<common::model::source_file>
|
||||||
translation_unit_context::get_current_file() const
|
translation_unit_context::get_current_file() const
|
||||||
{
|
{
|
||||||
return current_file_;
|
return current_file_;
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ public:
|
|||||||
clanguml::include_diagram::model::diagram &diagram();
|
clanguml::include_diagram::model::diagram &diagram();
|
||||||
|
|
||||||
void set_current_file(
|
void set_current_file(
|
||||||
type_safe::optional_ref<include_diagram::model::source_file> p);
|
type_safe::optional_ref<common::model::source_file> p);
|
||||||
|
|
||||||
type_safe::optional_ref<include_diagram::model::source_file>
|
type_safe::optional_ref<common::model::source_file>
|
||||||
get_current_file() const;
|
get_current_file() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -56,7 +56,7 @@ private:
|
|||||||
// Reference to class diagram config
|
// Reference to class diagram config
|
||||||
const clanguml::config::include_diagram &config_;
|
const clanguml::config::include_diagram &config_;
|
||||||
|
|
||||||
type_safe::optional_ref<include_diagram::model::source_file> current_file_;
|
type_safe::optional_ref<common::model::source_file> current_file_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
#include <cppast/cpp_entity_kind.hpp>
|
#include <cppast/cpp_entity_kind.hpp>
|
||||||
#include <cppast/cpp_preprocessor.hpp>
|
#include <cppast/cpp_preprocessor.hpp>
|
||||||
|
|
||||||
namespace clanguml::include_diagram::visitor {
|
#include <filesystem>
|
||||||
|
|
||||||
using clanguml::include_diagram::model::diagram;
|
namespace clanguml::include_diagram::visitor {
|
||||||
|
|
||||||
translation_unit_visitor::translation_unit_visitor(
|
translation_unit_visitor::translation_unit_visitor(
|
||||||
cppast::cpp_entity_index &idx,
|
cppast::cpp_entity_index &idx,
|
||||||
@@ -35,19 +35,116 @@ translation_unit_visitor::translation_unit_visitor(
|
|||||||
|
|
||||||
void translation_unit_visitor::operator()(const cppast::cpp_entity &file)
|
void translation_unit_visitor::operator()(const cppast::cpp_entity &file)
|
||||||
{
|
{
|
||||||
|
assert(file.kind() == cppast::cpp_entity_kind::file_t);
|
||||||
|
|
||||||
|
const auto &f = static_cast<const cppast::cpp_file &>(file);
|
||||||
|
|
||||||
|
auto file_path = f.name();
|
||||||
|
|
||||||
|
LOG_DBG("Processing source file {}", file_path);
|
||||||
|
|
||||||
|
process_file(file_path, true);
|
||||||
|
|
||||||
cppast::visit(file,
|
cppast::visit(file,
|
||||||
[&, this](const cppast::cpp_entity &e, cppast::visitor_info info) {
|
[&, this](const cppast::cpp_entity &e, cppast::visitor_info info) {
|
||||||
if (e.kind() == cppast::cpp_entity_kind::include_directive_t) {
|
if (e.kind() == cppast::cpp_entity_kind::include_directive_t) {
|
||||||
|
assert(ctx.get_current_file().has_value());
|
||||||
|
|
||||||
|
auto file_path_cpy = file.name();
|
||||||
|
|
||||||
const auto &inc =
|
const auto &inc =
|
||||||
static_cast<const cppast::cpp_include_directive &>(e);
|
static_cast<const cppast::cpp_include_directive &>(e);
|
||||||
|
|
||||||
auto file_name = std::filesystem::path(inc.full_path());
|
LOG_DBG("Processing include directive {} in file {}",
|
||||||
|
inc.full_path(), ctx.get_current_file().value().name());
|
||||||
|
|
||||||
auto f = std::make_unique<model::source_file>();
|
process_file(inc.full_path(), false, inc.include_kind());
|
||||||
f->set_path(file_name.parent_path().string());
|
|
||||||
f->set_name(file_name.filename().string());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void translation_unit_visitor::process_file(const std::string &file,
|
||||||
|
bool register_as_current,
|
||||||
|
std::optional<cppast::cpp_include_kind> include_kind)
|
||||||
|
{
|
||||||
|
auto include_path = std::filesystem::path(file);
|
||||||
|
|
||||||
|
const std::filesystem::path base_directory{ctx.config().base_directory()};
|
||||||
|
|
||||||
|
if (include_path.is_relative()) {
|
||||||
|
include_path = ctx.config().base_directory() / include_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
include_path = include_path.lexically_normal();
|
||||||
|
|
||||||
|
auto f_abs = std::make_unique<common::model::source_file>();
|
||||||
|
|
||||||
|
if (include_path.is_absolute())
|
||||||
|
f_abs->set_absolute();
|
||||||
|
|
||||||
|
f_abs->set_path(include_path.parent_path().string());
|
||||||
|
f_abs->set_name(include_path.filename().string());
|
||||||
|
|
||||||
|
if (ctx.diagram().should_include(*f_abs)) {
|
||||||
|
if (ctx.config().relative_to) {
|
||||||
|
const std::filesystem::path relative_to{ctx.config().relative_to()};
|
||||||
|
|
||||||
|
include_path = std::filesystem::relative(include_path, relative_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto f = std::make_unique<common::model::source_file>();
|
||||||
|
|
||||||
|
auto parent_path_str = include_path.parent_path().string();
|
||||||
|
if (!parent_path_str.empty())
|
||||||
|
f->set_path(parent_path_str);
|
||||||
|
f->set_name(include_path.filename().string());
|
||||||
|
f->set_type(common::model::source_file_t::kHeader);
|
||||||
|
|
||||||
|
if (register_as_current &&
|
||||||
|
ctx.diagram().get_element(f->path() | f->name()).has_value()) {
|
||||||
|
// This file is already in the diagram (e.g. it was added through an
|
||||||
|
// include directive before it was visited by the parser)
|
||||||
|
ctx.set_current_file(
|
||||||
|
ctx.diagram().get_element(f->path() | f->name()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!f->path().is_empty()) {
|
||||||
|
// Ensure parent path directories source_files
|
||||||
|
// are in the diagram
|
||||||
|
common::model::filesystem_path parent_path_so_far;
|
||||||
|
for (const auto &directory : f->path()) {
|
||||||
|
auto dir = std::make_unique<common::model::source_file>();
|
||||||
|
if (!parent_path_so_far.is_empty())
|
||||||
|
dir->set_path(parent_path_so_far);
|
||||||
|
dir->set_name(directory);
|
||||||
|
dir->set_type(common::model::source_file_t::kDirectory);
|
||||||
|
|
||||||
|
if (!ctx.diagram()
|
||||||
|
.get_element(parent_path_so_far | directory)
|
||||||
|
.has_value())
|
||||||
|
ctx.diagram().add_file(std::move(dir));
|
||||||
|
|
||||||
|
parent_path_so_far.append(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!register_as_current) {
|
||||||
|
auto relationship_type =
|
||||||
|
common::model::relationship_t::kAssociation;
|
||||||
|
if (include_kind.has_value() &&
|
||||||
|
include_kind.value() == cppast::cpp_include_kind::system)
|
||||||
|
relationship_type = common::model::relationship_t::kDependency;
|
||||||
|
|
||||||
|
ctx.get_current_file().value().add_relationship(
|
||||||
|
common::model::relationship{relationship_type, f->alias()});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.diagram().get_element(f->path() | f->name()).has_value()) {
|
||||||
|
ctx.set_current_file(type_safe::opt_ref(*f));
|
||||||
|
ctx.diagram().add_file(std::move(f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,15 @@
|
|||||||
#include "cx/cursor.h"
|
#include "cx/cursor.h"
|
||||||
#include "include_diagram/model/diagram.h"
|
#include "include_diagram/model/diagram.h"
|
||||||
#include "include_diagram/visitor/translation_unit_context.h"
|
#include "include_diagram/visitor/translation_unit_context.h"
|
||||||
|
#include "common/model/enums.h"
|
||||||
|
#include "common/model/package.h"
|
||||||
|
|
||||||
#include <clang-c/CXCompilationDatabase.h>
|
#include <clang-c/CXCompilationDatabase.h>
|
||||||
#include <clang-c/Index.h>
|
#include <clang-c/Index.h>
|
||||||
#include <cppast/visitor.hpp>
|
#include <cppast/visitor.hpp>
|
||||||
|
#include <cppast/cpp_preprocessor.hpp>
|
||||||
#include <type_safe/reference.hpp>
|
#include <type_safe/reference.hpp>
|
||||||
|
|
||||||
#include <common/model/enums.h>
|
|
||||||
#include <common/model/package.h>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -45,6 +46,9 @@ public:
|
|||||||
void operator()(const cppast::cpp_entity &file);
|
void operator()(const cppast::cpp_entity &file);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void process_file(const std::string &file, bool register_as_current = false,
|
||||||
|
std::optional<cppast::cpp_include_kind> include_kind = {});
|
||||||
|
|
||||||
// ctx allows to track current visitor context, e.g. current namespace
|
// ctx allows to track current visitor context, e.g. current namespace
|
||||||
translation_unit_context ctx;
|
translation_unit_context ctx;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -198,5 +198,37 @@ bool replace_all(
|
|||||||
return replaced;
|
return replaced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
bool starts_with(
|
||||||
|
const std::filesystem::path &path, const std::filesystem::path &prefix)
|
||||||
|
{
|
||||||
|
if(path == prefix)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const int path_length = std::distance(std::begin(path), std::end(path));
|
||||||
|
|
||||||
|
auto last_nonempty_prefix_element = std::prev(std::find_if(
|
||||||
|
prefix.begin(), prefix.end(), [](auto &&n) { return n.empty(); }));
|
||||||
|
|
||||||
|
int prefix_length =
|
||||||
|
std::distance(std::begin(prefix), last_nonempty_prefix_element);
|
||||||
|
|
||||||
|
// Empty prefix always matches
|
||||||
|
if (prefix_length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Prefix longer then path never matches
|
||||||
|
if (prefix_length >= path_length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto path_compare_end = path.begin();
|
||||||
|
std::advance(path_compare_end, prefix_length);
|
||||||
|
|
||||||
|
std::vector<std::string> pref(prefix.begin(), last_nonempty_prefix_element);
|
||||||
|
std::vector<std::string> pat(path.begin(), path_compare_end);
|
||||||
|
|
||||||
|
return pref == pat;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
@@ -143,23 +144,26 @@ template <typename T> void append(std::vector<T> &l, const std::vector<T> &r)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Checks if vector starts with a prefix.
|
* @brief Checks if container starts with a prefix.
|
||||||
*
|
*
|
||||||
* @tparam T
|
* @tparam T e.g. std::vector<std::string>
|
||||||
* @param col
|
* @param con Container to be checked against prefix
|
||||||
* @param prefix
|
* @param prefix Container, which specifies the prefix
|
||||||
* @return
|
* @return true if first prefix.size() elements of con are equal to prefix
|
||||||
*/
|
*/
|
||||||
template <typename T>
|
template <typename T> bool starts_with(const T &con, const T &prefix)
|
||||||
bool starts_with(const std::vector<T> &col, const std::vector<T> &prefix)
|
|
||||||
{
|
{
|
||||||
if (prefix.size() > col.size())
|
if (prefix.size() > con.size())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return std::vector<std::string>(prefix.begin(), prefix.end()) ==
|
return T(prefix.begin(), prefix.end()) ==
|
||||||
std::vector<std::string>(col.begin(), col.begin() + prefix.size());
|
T(con.begin(), con.begin() + prefix.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
bool starts_with(
|
||||||
|
const std::filesystem::path &path, const std::filesystem::path &prefix);
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
bool ends_with(const std::vector<T> &col, const std::vector<T> &suffix)
|
bool ends_with(const std::vector<T> &col, const std::vector<T> &suffix)
|
||||||
{
|
{
|
||||||
|
|||||||
9
tests/t40001/include/t40001_include1.h
Normal file
9
tests/t40001/include/t40001_include1.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "lib1/lib1.h"
|
||||||
|
|
||||||
|
namespace clanguml::t40001 {
|
||||||
|
|
||||||
|
int foo() { return lib1::foo2(); }
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "t40001_include1.h"
|
#include "include/t40001_include1.h"
|
||||||
|
|
||||||
namespace clanguml {
|
namespace clanguml {
|
||||||
namespace t40001 {
|
namespace t40001 {
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
namespace clanguml::t40001 {
|
|
||||||
|
|
||||||
int foo() { return 0; }
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user