Fixed handling of nested lambda expressions in sequence diagrams

This commit is contained in:
Bartek Kryza
2024-04-28 00:57:46 +02:00
parent 0539fb0101
commit efc34bcec6
13 changed files with 407 additions and 50 deletions

View File

@@ -132,6 +132,9 @@ void call_expression_context::update(
std::int64_t call_expression_context::caller_id() const
{
if (lambda_caller_id() != 0)
return lambda_caller_id();
return current_caller_id_;
}

View File

@@ -455,8 +455,10 @@ bool translation_unit_visitor::VisitLambdaExpr(clang::LambdaExpr *expr)
lambda_method_model_ptr->set_method_name(method_name);
lambda_method_model_ptr->set_class_id(cls_id);
lambda_method_model_ptr->set_class_full_name(
lambda_class_model_ptr->full_name(false));
// If this is a nested lambda, prepend the parent lambda name to this lambda
auto lambda_class_full_name = lambda_class_model_ptr->full_name(false);
lambda_method_model_ptr->set_class_full_name(lambda_class_full_name);
diagram().add_participant(std::move(lambda_class_model_ptr));
@@ -469,9 +471,8 @@ bool translation_unit_visitor::VisitLambdaExpr(clang::LambdaExpr *expr)
// If lambda expression is in an argument to a method/function, and that
// method function would be excluded by filters
if (std::holds_alternative<clang::CallExpr *>(
context().current_callexpr())/* &&
!should_include(
std::get<clang::CallExpr *>(context().current_callexpr()))*/) {
context().current_callexpr()) &&
(context().lambda_caller_id() == 0)) {
using clanguml::common::model::message_t;
using clanguml::sequence_diagram::model::message;
@@ -504,9 +505,6 @@ bool translation_unit_visitor::VisitLambdaExpr(clang::LambdaExpr *expr)
bool translation_unit_visitor::TraverseLambdaExpr(clang::LambdaExpr *expr)
{
const auto lambda_full_name =
expr->getLambdaClass()->getCanonicalDecl()->getNameAsString();
RecursiveASTVisitor<translation_unit_visitor>::TraverseLambdaExpr(expr);
// lambda context is entered inside the visitor
@@ -543,10 +541,8 @@ bool translation_unit_visitor::TraverseCXXMemberCallExpr(
if (source_manager().isInSystemHeader(expr->getSourceRange().getBegin()))
return true;
LOG_DBG("Entering member call expression at {} to {}::{}",
expr->getBeginLoc().printToString(source_manager()),
common::to_string(expr->getObjectType(), context().get_ast_context()),
common::to_string(expr->getMethodDecl()));
LOG_DBG("Entering member call expression at {}",
expr->getBeginLoc().printToString(source_manager()));
context().enter_callexpr(expr);
@@ -599,6 +595,9 @@ bool translation_unit_visitor::TraverseCXXTemporaryObjectExpr(
bool translation_unit_visitor::TraverseCXXConstructExpr(
clang::CXXConstructExpr *expr)
{
LOG_DBG("Entering cxx construct call expression at {}",
expr->getBeginLoc().printToString(source_manager()));
context().enter_callexpr(expr);
RecursiveASTVisitor<translation_unit_visitor>::TraverseCXXConstructExpr(
@@ -606,6 +605,9 @@ bool translation_unit_visitor::TraverseCXXConstructExpr(
translation_unit_visitor::VisitCXXConstructExpr(expr);
LOG_DBG("Leaving member call expression at {}",
expr->getBeginLoc().printToString(source_manager()));
context().leave_callexpr();
pop_message_to_diagram(expr);
@@ -1057,13 +1059,6 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr)
if (!generated_message_from_comment && !should_include(expr))
return true;
// If we're currently inside a lambda expression, set it's id as
// message source rather then enclosing context
// Unless the lambda is declared in a function or method call
if (context().lambda_caller_id() != 0) {
m.set_from(context().lambda_caller_id());
}
if (context().is_expr_in_current_control_statement_condition(expr)) {
m.set_message_scope(common::model::message_scope_t::kCondition);
}
@@ -1100,9 +1095,12 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr)
if (callee_decl == nullptr) {
LOG_DBG("Cannot get callee declaration - trying direct function "
"callee...");
callee_decl = expr->getDirectCallee();
LOG_DBG(
"Found function/method callee in: {}", common::to_string(expr));
if (callee_decl != nullptr)
LOG_DBG("Found function/method callee in: {}",
common::to_string(expr));
}
if (callee_decl == nullptr) {
@@ -1124,6 +1122,17 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr)
if (!process_unresolved_lookup_call_expression(m, expr))
return true;
}
else if (clang::dyn_cast_or_null<clang::LambdaExpr>(
expr->getCallee()) != nullptr) {
LOG_DBG("Processing lambda expression callee");
if (!process_lambda_call_expression(m, expr))
return true;
}
else {
LOG_DBG("Found unsupported callee decl type for: {} at {}",
common::to_string(expr),
expr->getBeginLoc().printToString(source_manager()));
}
}
else {
auto success = process_function_call_expression(m, expr);
@@ -1219,10 +1228,6 @@ bool translation_unit_visitor::VisitCXXConstructExpr(
set_source_location(*expr, m);
if (context().lambda_caller_id() != 0) {
m.set_from(context().lambda_caller_id());
}
if (context().is_expr_in_current_control_statement_condition(expr)) {
m.set_message_scope(common::model::message_scope_t::kCondition);
}
@@ -1487,6 +1492,26 @@ bool translation_unit_visitor::process_function_call_expression(
return true;
}
bool translation_unit_visitor::process_lambda_call_expression(
model::message &m, const clang::CallExpr *expr) const
{
const auto *lambda_expr =
clang::dyn_cast_or_null<clang::LambdaExpr>(expr->getCallee());
if (lambda_expr == nullptr)
return true;
const auto lambda_class_id = lambda_expr->getLambdaClass()->getID();
const auto maybe_id = get_unique_id(lambda_class_id);
if (!maybe_id.has_value())
m.set_to(lambda_class_id);
else {
m.set_to(maybe_id.value());
}
return true;
}
bool translation_unit_visitor::process_unresolved_lookup_call_expression(
model::message &m, const clang::CallExpr *expr) const
{
@@ -1498,7 +1523,6 @@ bool translation_unit_visitor::process_unresolved_lookup_call_expression(
for (const auto *decl : unresolved_expr->decls()) {
if (clang::dyn_cast_or_null<clang::FunctionTemplateDecl>(decl) !=
nullptr) {
// Yes, it's a template
const auto *ftd =
clang::dyn_cast_or_null<clang::FunctionTemplateDecl>(decl);
@@ -1511,6 +1535,23 @@ bool translation_unit_visitor::process_unresolved_lookup_call_expression(
break;
}
else if (clang::dyn_cast_or_null<clang::FunctionDecl>(decl) !=
nullptr) {
const auto *fd =
clang::dyn_cast_or_null<clang::FunctionDecl>(decl);
const auto maybe_id = get_unique_id(fd->getID());
if (!maybe_id.has_value())
m.set_to(fd->getID());
else {
m.set_to(maybe_id.value());
}
break;
}
else {
LOG_DBG("Unknown unresolved lookup expression");
}
}
}
@@ -1568,9 +1609,10 @@ translation_unit_visitor::create_class_model(clang::CXXRecordDecl *cls)
const auto *parent = cls->getParent();
if ((parent != nullptr) && parent->isRecord()) {
// Here we have 2 options, either:
// Here we have 3 options, either:
// - the parent is a regular C++ class/struct
// - the parent is a class template declaration/specialization
// - the parent is a lambda (i.e. this is a nested lambda expression)
std::optional<common::id_t> id_opt;
const auto *parent_record_decl =
clang::dyn_cast<clang::RecordDecl>(parent);
@@ -1817,7 +1859,31 @@ std::string translation_unit_visitor::make_lambda_name(
const auto location = cls->getLocation();
const std::string source_location{lambda_source_location(location)};
if (context().caller_id() != 0 &&
if (context().lambda_caller_id() != 0) {
// Parent is also a lambda (this id points to a lambda operator())
std::string parent_lambda_class_name{"()"};
if (diagram().get_participant<model::method>(
context().lambda_caller_id())) {
auto parent_lambda_class_id = diagram()
.get_participant<model::method>(
context().lambda_caller_id())
.value()
.class_id();
if (diagram().get_participant<model::class_>(
parent_lambda_class_id)) {
parent_lambda_class_name =
diagram()
.get_participant<model::class_>(parent_lambda_class_id)
.value()
.full_name(false);
}
}
result = fmt::format(
"{}##(lambda {})", parent_lambda_class_name, source_location);
}
else if (context().caller_id() != 0 &&
get_participant(context().caller_id()).has_value()) {
auto parent_full_name =
get_participant(context().caller_id()).value().full_name_no_ns();

View File

@@ -436,6 +436,9 @@ private:
bool process_unresolved_lookup_call_expression(
model::message &m, const clang::CallExpr *expr) const;
bool process_lambda_call_expression(
model::message &m, const clang::CallExpr *expr) const;
/**
* @brief Register a message model `m` with a call expression
*

13
tests/t20045/.clang-uml Normal file
View File

@@ -0,0 +1,13 @@
add_compile_flags:
- -fparse-all-comments
diagrams:
t20045_sequence:
type: sequence
glob:
- t20045.cc
include:
namespaces:
- clanguml::t20045
using_namespace: clanguml::t20045
from:
- function: "clanguml::t20045::tmain()"

47
tests/t20045/t20045.cc Normal file
View File

@@ -0,0 +1,47 @@
namespace clanguml {
namespace t20045 {
template <typename F> int a1(F &&f) { return f(42); }
int a2(int x) { return 2; }
int a3(int x) { return 3; }
struct B {
int b1(int x) { return x + 1; }
int b2(int x) { return x + 2; }
};
class C {
public:
explicit C(int x)
: x_{x}
{
}
int get_x() const { return x_; }
private:
int x_;
};
int tmain()
{
B b;
// \uml{call clanguml::t20045::a2(int)}
auto v1 = a1(a2);
auto v2 = a1([](auto &&arg) { return a3(arg); });
auto v3 = a1([&](auto &&arg) { return b.b1(arg); });
auto v4 = a1([](auto &&arg) {
C c(arg);
return c.get_x();
});
return 0;
}
}
}

82
tests/t20045/test_case.h Normal file
View File

@@ -0,0 +1,82 @@
/**
* tests/t20045/test_case.h
*
* Copyright (c) 2021-2024 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.
*/
TEST_CASE("t20045", "[test-case][sequence]")
{
auto [config, db] = load_config("t20045");
auto diagram = config.diagrams["t20045_sequence"];
REQUIRE(diagram->name == "t20045_sequence");
auto model = generate_sequence_diagram(*db, diagram);
REQUIRE(model->name() == "t20045_sequence");
{
auto src = generate_sequence_puml(diagram, *model);
AliasMatcher _A(src);
REQUIRE_THAT(src, StartsWith("@startuml"));
REQUIRE_THAT(src, EndsWith("@enduml\n"));
REQUIRE_THAT(src, HasCall(_A("tmain()"), _A("a2(int)"), ""));
REQUIRE_THAT(src,
HasCall(_A("tmain()"),
_A("a1<(lambda at t20045.cc:35:18)>((lambda at "
"t20045.cc:35:18) &&)"),
""));
REQUIRE_THAT(src,
HasCall(_A("a1<(lambda at t20045.cc:35:18)>((lambda at "
"t20045.cc:35:18) &&)"),
_A("tmain()::(lambda t20045.cc:35:18)"), "operator()()"));
REQUIRE_THAT(src,
HasCall(
_A("tmain()::(lambda t20045.cc:35:18)"), _A("a3(int)"), ""));
REQUIRE_THAT(src,
HasCall(
_A("tmain()::(lambda t20045.cc:37:18)"), _A("B"), "b1(int)"));
REQUIRE_THAT(src,
HasCall(
_A("tmain()::(lambda t20045.cc:39:18)"), _A("C"), "get_x()"));
save_puml(config.output_directory(), diagram->name + ".puml", src);
}
{
auto j = generate_sequence_json(diagram, *model);
using namespace json;
save_json(config.output_directory(), diagram->name + ".json", j);
}
{
auto src = generate_sequence_mermaid(diagram, *model);
mermaid::AliasMatcher _A(src);
using mermaid::IsClass;
save_mermaid(config.output_directory(), diagram->name + ".mmd", src);
}
}

13
tests/t20046/.clang-uml Normal file
View File

@@ -0,0 +1,13 @@
add_compile_flags:
- -fparse-all-comments
diagrams:
t20046_sequence:
type: sequence
glob:
- t20046.cc
include:
namespaces:
- clanguml::t20046
using_namespace: clanguml::t20046
from:
- function: "clanguml::t20046::tmain()"

24
tests/t20046/t20046.cc Normal file
View File

@@ -0,0 +1,24 @@
namespace clanguml {
namespace t20046 {
template <typename F> int a1(F &&f) { return f(42); }
int a2(int x) { return 2; }
int a3(int x) { return 3; }
int tmain()
{
// Call expression in a nested lambda
auto v1 = [](auto &&arg1) {
return [](auto &&arg2) { return a2(arg2); }(arg1);
}(0);
// Call expression in a nested lambda in call expression
auto v4 = a1(
[](auto &&arg1) { return [](auto &&arg2) { return a3(arg2); }(arg1); });
return 0;
}
}
}

94
tests/t20046/test_case.h Normal file
View File

@@ -0,0 +1,94 @@
/**
* tests/t20046/test_case.h
*
* Copyright (c) 2021-2024 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.
*/
TEST_CASE("t20046", "[test-case][sequence]")
{
auto [config, db] = load_config("t20046");
auto diagram = config.diagrams["t20046_sequence"];
REQUIRE(diagram->name == "t20046_sequence");
auto model = generate_sequence_diagram(*db, diagram);
REQUIRE(model->name() == "t20046_sequence");
{
auto src = generate_sequence_puml(diagram, *model);
AliasMatcher _A(src);
REQUIRE_THAT(src, StartsWith("@startuml"));
REQUIRE_THAT(src, EndsWith("@enduml\n"));
REQUIRE_THAT(src,
HasCall(_A("tmain()"), _A("tmain()::(lambda t20046.cc:13:15)"),
"operator()()"));
REQUIRE_THAT(src,
HasCall(_A("tmain()::(lambda t20046.cc:13:15)"),
_A("tmain()::(lambda t20046.cc:13:15)::(lambda "
"t20046.cc:14:16)"),
"operator()()"));
REQUIRE_THAT(src,
HasCall(_A("tmain()::(lambda t20046.cc:13:15)::(lambda "
"t20046.cc:14:16)"),
_A("a2(int)"), ""));
REQUIRE_THAT(src,
HasCall(_A("tmain()"),
_A("a1<(lambda at t20046.cc:19:9)>((lambda at t20046.cc:19:9) "
"&&)"),
""));
REQUIRE_THAT(src,
HasCall(
_A("a1<(lambda at t20046.cc:19:9)>((lambda at t20046.cc:19:9) "
"&&)"),
_A("tmain()::(lambda t20046.cc:19:9)"), "operator()()"));
REQUIRE_THAT(src,
HasCall(_A("tmain()::(lambda t20046.cc:19:9)"),
_A("tmain()::(lambda t20046.cc:19:9)::(lambda "
"t20046.cc:19:34)"),
"operator()()"));
REQUIRE_THAT(src,
HasCall(_A("tmain()::(lambda t20046.cc:19:9)::(lambda "
"t20046.cc:19:34)"),
_A("a3(int)"), ""));
save_puml(config.output_directory(), diagram->name + ".puml", src);
}
{
auto j = generate_sequence_json(diagram, *model);
using namespace json;
save_json(config.output_directory(), diagram->name + ".json", j);
}
{
auto src = generate_sequence_mermaid(diagram, *model);
mermaid::AliasMatcher _A(src);
using mermaid::IsClass;
save_mermaid(config.output_directory(), diagram->name + ".mmd", src);
}
}

View File

@@ -471,6 +471,8 @@ using namespace clanguml::test::matchers;
#include "t20042/test_case.h"
#include "t20043/test_case.h"
#include "t20044/test_case.h"
#include "t20045/test_case.h"
#include "t20046/test_case.h"
///
/// Package diagram tests

View File

@@ -400,7 +400,7 @@ struct AliasMatcher {
}
}
return "__INVALID__ALIAS__";
return fmt::format("__INVALID__ALIAS__({})", name);
}
const std::vector<std::string> puml;

View File

@@ -352,6 +352,15 @@ test_cases:
- name: t20043
title: Test case for elements diagram filter in sequence diagrams
description:
- name: t20044
title: Test case for template method call expressions with callables
description:
- name: t20045
title: Test case for template function call expressions with callables
description:
- name: t20046
title: Test case for call expressions in nested lambdas
description:
Package diagrams:
- name: t30001
title: Basic package diagram test case

View File

@@ -26,63 +26,64 @@ TEST_CASE_MULTIPLIER = 10000
CLASS_DIAGRAM_TEST_CASE_EXAMPLES = """
// Check if all classes exist
//REQUIRE_THAT(puml, IsClass(_A("A")));
//REQUIRE_THAT(src, IsClass(_A("A")));
// Check if class templates exist
//REQUIRE_THAT(puml, IsClassTemplate("A", "T,P,CMP,int N"));
//REQUIRE_THAT(src, IsClassTemplate("A", "T,P,CMP,int N"));
// Check concepts
//REQUIRE_THAT(puml, IsConcept(_A("AConcept<T>")));
//REQUIRE_THAT(puml,
//REQUIRE_THAT(src, IsConcept(_A("AConcept<T>")));
//REQUIRE_THAT(src,
// IsConceptRequirement(
// _A("AConcept<T,P>"), "sizeof (T) > sizeof (P)"));
// Check if all enums exist
//REQUIRE_THAT(puml, IsEnum(_A("Lights")));
//REQUIRE_THAT(src, IsEnum(_A("Lights")));
// Check if all inner classes exist
//REQUIRE_THAT(puml, IsInnerClass(_A("A"), _A("AA")));
//REQUIRE_THAT(src, IsInnerClass(_A("A"), _A("AA")));
// Check if all inheritance relationships exist
//REQUIRE_THAT(puml, IsBaseClass(_A("Base"), _A("Child")));
//REQUIRE_THAT(src, IsBaseClass(_A("Base"), _A("Child")));
// Check if all methods exist
//REQUIRE_THAT(puml, (IsMethod<Public, Const>("foo")));
//REQUIRE_THAT(src, (IsMethod<Public, Const>("foo")));
// Check if all fields exist
//REQUIRE_THAT(puml, (IsField<Private>("private_member", "int")));
//REQUIRE_THAT(src, (IsField<Private>("private_member", "int")));
// Check if all relationships exist
//REQUIRE_THAT(puml, IsAssociation(_A("D"), _A("A"), "-as"));
//REQUIRE_THAT(puml, IsDependency(_A("R"), _A("B")));
//REQUIRE_THAT(puml, IsAggregation(_A("R"), _A("D"), "-ag"));
//REQUIRE_THAT(puml, IsComposition(_A("R"), _A("D"), "-ac"));
//REQUIRE_THAT(puml, IsInstantiation(_A("ABCD::F<T>"), _A("F<int>")));
//REQUIRE_THAT(src, IsAssociation(_A("D"), _A("A"), "-as"));
//REQUIRE_THAT(src, IsDependency(_A("R"), _A("B")));
//REQUIRE_THAT(src, IsAggregation(_A("R"), _A("D"), "-ag"));
//REQUIRE_THAT(src, IsComposition(_A("R"), _A("D"), "-ac"));
//REQUIRE_THAT(src, IsInstantiation(_A("ABCD::F<T>"), _A("F<int>")));
"""
SEQUENCE_DIAGRAM_TEST_CASE_EXAMPLES = """
// Check if all calls exist
//REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "a()"));
//REQUIRE_THAT(puml, HasCall(_A("A"), "a()"));
//REQUIRE_THAT(src, HasCall(_A("tmain()"), _A("A"), "a()"));
//REQUIRE_THAT(src, HasCall(_A("A"), "a()"));
"""
PACKAGE_DIAGRAM_TEST_CASE_EXAMPLES = """
// Check if all packages exist
//REQUIRE_THAT(puml, IsPackage("ns1"));
//REQUIRE_THAT(src, IsPackage("ns1"));
"""
INCLUDE_DIAGRAM_TEST_CASE_EXAMPLES = """
// Check all folders exist
//REQUIRE_THAT(puml, IsFolder("lib1"));
//REQUIRE_THAT(src, IsFolder("lib1"));
// Check if all files exist
//REQUIRE_THAT(puml, IsFile("lib1.h"));
//REQUIRE_THAT(src, IsFile("lib1.h"));
// Check if all includes exists
//REQUIRE_THAT(puml, IsAssociation(_A("t40002.cc"), _A("lib1.h")));
//REQUIRE_THAT(puml, IsDependency(_A("t40001_include1.h"), _A("string")));
//REQUIRE_THAT(src, IsAssociation(_A("t40002.cc"), _A("lib1.h")));
//REQUIRE_THAT(src, IsDependency(_A("t40001_include1.h"), _A("string")));
"""
def test_case_already_exists(name):
return os.path.isdir(os.path.join(os.path.dirname(__file__), '..', name))