diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f7df77b6..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - // General Settings - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "editor.rulers": [88], - - // Python Settings - "[python]": { - // Opinionated option for the future: - // "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.sortImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff" - }, - "python.analysis.typeCheckingMode": "off", - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true -} diff --git a/cpp/libclang/docs/ast-traversal.md b/cpp/libclang/docs/ast-traversal.md index d69f90f1..65189674 100644 --- a/cpp/libclang/docs/ast-traversal.md +++ b/cpp/libclang/docs/ast-traversal.md @@ -45,7 +45,7 @@ not filtered out, it dispatches to the relevant specialized visitor: | --- | --- | | `ClassDecl`, `StructDecl`, `ClassTemplate`, and `ClassTemplatePartialSpecialization` | Extract class/struct entities, members, aliases, bases, and relationship inputs. | | `EnumDecl` | Extract enum entities and literals. | -| `FunctionDecl`, `FunctionTemplate`, and `Method` | Extract callable definitions and their body control flow. Function templates are classified as free functions, methods, or static methods according to their scope. | +| `FunctionDecl`, `FunctionTemplate`, `Method`, `Constructor`, and `Destructor` | Extract callable definitions and their body control flow. Function templates are classified as free functions, methods, or static methods according to their scope. Constructors and destructors are routed through the same callable visitor and participate in body extraction when they have a direct compound body. | After traversal, class relationship resolution uses the collected base, variable, and method type information to populate the class-diagram diff --git a/cpp/libclang/docs/function-extraction.md b/cpp/libclang/docs/function-extraction.md index 8af4f75a..4e96d2f5 100644 --- a/cpp/libclang/docs/function-extraction.md +++ b/cpp/libclang/docs/function-extraction.md @@ -195,15 +195,17 @@ The top-level visitor currently dispatches these cursor kinds to | `FunctionDecl` | `Free` | | `FunctionTemplate` | `Free` at global or namespace scope; `Method` or `StaticMethod` at type scope | | `Method` | `Method` or `StaticMethod` | +| `Constructor` | `Constructor` | +| `Destructor` | `Destructor` | C++ member operator overloads such as `operator+` and `operator[]` are normally reported as `Method`; the current model does not use a distinct operator-method kind. `FunctionVisitor` has internal kind mappings for `Constructor`, `Destructor`, -and `ConversionFunction`, but the top-level visitor currently logs and ignores -those cursor kinds. Therefore they do not currently produce `FunctionDef` -entries. A conversion operator such as `operator bool()` is a +and `ConversionFunction`. The top-level visitor currently dispatches +constructors and destructors for extraction, but still logs and ignores +`ConversionFunction`. A conversion operator such as `operator bool()` is a `ConversionFunction` and is distinct from a normal operator overload. Namespace-level function templates are extracted as `FunctionDef` entries with diff --git a/cpp/libclang/integration_test/cases/definition_then_forward_decl/BUILD b/cpp/libclang/integration_test/cases/definition_then_forward_decl/BUILD new file mode 100644 index 00000000..121df8d0 --- /dev/null +++ b/cpp/libclang/integration_test/cases/definition_then_forward_decl/BUILD @@ -0,0 +1,32 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("//cpp/libclang/integration_test:test_rules.bzl", "cpp_parser_integration_test") + +cc_library( + name = "definition_then_forward_decl", + srcs = [ + "first.cpp", + "second.cpp", + ], + hdrs = [ + "widget_forward.h", + "widget_full.h", + ], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_definition_then_forward_decl", + expected_output = ["expected.json"], + target = ":definition_then_forward_decl", +) diff --git a/cpp/libclang/integration_test/cases/definition_then_forward_decl/expected.json b/cpp/libclang/integration_test/cases/definition_then_forward_decl/expected.json new file mode 100644 index 00000000..afdac130 --- /dev/null +++ b/cpp/libclang/integration_test/cases/definition_then_forward_decl/expected.json @@ -0,0 +1,33 @@ +{ + "types": { + "util::Widget": { + "id": "util::Widget", + "name": "Widget", + "enclosing_namespace_id": "util", + "stereotypes": [], + "entity_type": "Class", + "type_aliases": [], + "variables": [ + { + "name": "value", + "data_type": "int", + "visibility": "public", + "is_static": false, + "source_location": { + "file": "cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_full.h", + "line": 19 + } + } + ], + "methods": [], + "template_parameters": null, + "enum_literals": [], + "relationships": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_full.h", + "line": 17 + } + } + }, + "functions": [] +} diff --git a/cpp/libclang/integration_test/cases/definition_then_forward_decl/first.cpp b/cpp/libclang/integration_test/cases/definition_then_forward_decl/first.cpp new file mode 100644 index 00000000..b1d38b63 --- /dev/null +++ b/cpp/libclang/integration_test/cases/definition_then_forward_decl/first.cpp @@ -0,0 +1,14 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "widget_forward.h" diff --git a/cpp/libclang/integration_test/cases/definition_then_forward_decl/run_test.rs b/cpp/libclang/integration_test/cases/definition_then_forward_decl/run_test.rs new file mode 100644 index 00000000..6579d290 --- /dev/null +++ b/cpp/libclang/integration_test/cases/definition_then_forward_decl/run_test.rs @@ -0,0 +1,19 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use test_framework::run_parser_case; + +#[test] +fn test_definition_then_forward_decl() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/cases/definition_then_forward_decl/second.cpp b/cpp/libclang/integration_test/cases/definition_then_forward_decl/second.cpp new file mode 100644 index 00000000..9b58f0bf --- /dev/null +++ b/cpp/libclang/integration_test/cases/definition_then_forward_decl/second.cpp @@ -0,0 +1,14 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "widget_full.h" diff --git a/cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_forward.h b/cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_forward.h new file mode 100644 index 00000000..f1644243 --- /dev/null +++ b/cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_forward.h @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +namespace util { +class Widget; +} // namespace util diff --git a/cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_full.h b/cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_full.h new file mode 100644 index 00000000..4c8804bc --- /dev/null +++ b/cpp/libclang/integration_test/cases/definition_then_forward_decl/widget_full.h @@ -0,0 +1,21 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +namespace util { +class Widget { +public: + int value; +}; +} // namespace util diff --git a/cpp/libclang/integration_test/cases/dependent_decltype_base/expected.json b/cpp/libclang/integration_test/cases/dependent_decltype_base/expected.json index 60d04f3f..3691259d 100644 --- a/cpp/libclang/integration_test/cases/dependent_decltype_base/expected.json +++ b/cpp/libclang/integration_test/cases/dependent_decltype_base/expected.json @@ -148,5 +148,60 @@ } } }, + "free_function_declarations": [ + { + "name": "declval", + "enclosing_namespace_id": null, + "return_type": "T", + "parameters": [], + "template_parameters": [ + { + "Type": { + "name": "T", + "is_pack": false + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 19 + } + }, + { + "name": "is_maplike_container_impl", + "enclosing_namespace_id": null, + "return_type": "decltype(value.begin())", + "parameters": [ + { + "name": "value", + "param_type": "T", + "is_variadic": false + } + ], + "template_parameters": [ + { + "Type": { + "name": "T", + "is_pack": false + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 22 + } + }, + { + "name": "make_widget", + "enclosing_namespace_id": null, + "return_type": "Widget", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 43 + } + } + ], "functions": [] } diff --git a/cpp/libclang/integration_test/function_cases/class_method_template_body/expected.json b/cpp/libclang/integration_test/function_cases/class_method_template_body/expected.json index 27632630..ec6797c9 100644 --- a/cpp/libclang/integration_test/function_cases/class_method_template_body/expected.json +++ b/cpp/libclang/integration_test/function_cases/class_method_template_body/expected.json @@ -1,4 +1,17 @@ { + "free_function_declarations": [ + { + "name": "notify", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/class_method_template_body/functions.cpp", + "line": 14 + } + } + ], "functions": [ { "id": { diff --git a/cpp/libclang/integration_test/function_cases/explicit_specialization/expected.json b/cpp/libclang/integration_test/function_cases/explicit_specialization/expected.json index 810b7872..b083b7cf 100644 --- a/cpp/libclang/integration_test/function_cases/explicit_specialization/expected.json +++ b/cpp/libclang/integration_test/function_cases/explicit_specialization/expected.json @@ -1,13 +1,57 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "specialized", + "enclosing_namespace_id": "utility", + "return_type": "T", + "parameters": [ + { + "name": "value", + "param_type": "T", + "is_variadic": false + } + ], + "template_parameters": [ + { + "Type": { + "name": "T", + "is_pack": false + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/explicit_specialization/specialization.h", + "line": 21 + } + }, + { + "name": "specialized", + "enclosing_namespace_id": "utility", + "return_type": "int", + "parameters": [ + { + "name": "value", + "param_type": "int", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/explicit_specialization/specialization.h", + "line": 25 + } + } + ], "functions": [ { "id": { - "name": "specialized", "scope": { "Namespace": [ "utility" ] - } + }, + "name": "specialized" }, "kind": "Free", "return_type": { @@ -15,6 +59,5 @@ }, "body": [] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/BUILD b/cpp/libclang/integration_test/function_cases/free_function_identity/BUILD index 63de8b23..e4d6e6cb 100644 --- a/cpp/libclang/integration_test/function_cases/free_function_identity/BUILD +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/BUILD @@ -14,7 +14,8 @@ load("//cpp/libclang/integration_test:test_rules.bzl", "cpp_parser_integration_t cc_library( name = "free_function_identity", - srcs = glob(["*.cpp"]), + srcs = ["functions.cpp"], + hdrs = ["functions.hpp"], visibility = ["//cpp/libclang:__subpackages__"], ) diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/expected.json b/cpp/libclang/integration_test/function_cases/free_function_identity/expected.json index f24885bc..ef0778f4 100644 --- a/cpp/libclang/integration_test/function_cases/free_function_identity/expected.json +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/expected.json @@ -1,4 +1,50 @@ { + "free_function_declarations": [ + { + "name": "declared_only", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/free_function_identity/functions.hpp", + "line": 16 + } + }, + { + "name": "global_value", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/free_function_identity/functions.hpp", + "line": 18 + } + }, + { + "name": "run", + "enclosing_namespace_id": "app", + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/free_function_identity/functions.hpp", + "line": 23 + } + }, + { + "name": "enabled", + "enclosing_namespace_id": "app::internal", + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp", + "line": 28 + } + } + ], "functions": [ { "id": { diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp b/cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp index df8ab3a9..174a79ce 100644 --- a/cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp @@ -11,7 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -void declared_only(); +#include "functions.hpp" int global_value() { return 1; diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/functions.hpp b/cpp/libclang/integration_test/function_cases/free_function_identity/functions.hpp new file mode 100644 index 00000000..990b7f3f --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/functions.hpp @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +void declared_only(); + +int global_value(); + +namespace app +{ + +void run(); + +} // namespace app diff --git a/cpp/libclang/integration_test/function_cases/free_function_template/expected.json b/cpp/libclang/integration_test/function_cases/free_function_template/expected.json index f6b56ecc..1edab3ae 100644 --- a/cpp/libclang/integration_test/function_cases/free_function_template/expected.json +++ b/cpp/libclang/integration_test/function_cases/free_function_template/expected.json @@ -1,4 +1,58 @@ { + "free_function_declarations": [ + { + "name": "consume", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [ + { + "name": "", + "param_type": "int", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/free_function_template/optional.h", + "line": 14 + } + }, + { + "name": "make_optional", + "enclosing_namespace_id": "amp", + "return_type": "int", + "parameters": [ + { + "name": "value", + "param_type": "T &&", + "is_variadic": false + } + ], + "template_parameters": [ + { + "Type": { + "name": "T", + "is_pack": false + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/free_function_template/optional.h", + "line": 18 + } + }, + { + "name": "run", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/free_function_template/functions.cpp", + "line": 18 + } + } + ], "functions": [ { "id": { diff --git a/cpp/libclang/integration_test/function_cases/guard_associativity/expected.json b/cpp/libclang/integration_test/function_cases/guard_associativity/expected.json index 677d3592..c3f8119c 100644 --- a/cpp/libclang/integration_test/function_cases/guard_associativity/expected.json +++ b/cpp/libclang/integration_test/function_cases/guard_associativity/expected.json @@ -1,13 +1,207 @@ { + "types": { + "Flag": { + "id": "Flag", + "name": "Flag", + "enclosing_namespace_id": null, + "stereotypes": [], + "entity_type": "Struct", + "type_aliases": [], + "variables": [], + "methods": [], + "template_parameters": null, + "enum_literals": [], + "relationships": [], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 30 + } + } + }, + "free_function_declarations": [ + { + "name": "first", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 14 + } + }, + { + "name": "second", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 15 + } + }, + { + "name": "third", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 16 + } + }, + { + "name": "handle_and", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 17 + } + }, + { + "name": "handle_or", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 18 + } + }, + { + "name": "handle_mixed", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 19 + } + }, + { + "name": "handle_alternative", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 20 + } + }, + { + "name": "handle_template_operand", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 21 + } + }, + { + "name": "handle_operator_function", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 22 + } + }, + { + "name": "check_template", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": [ + { + "NonType": { + "name": "", + "value_type": "bool", + "is_pack": false + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 25 + } + }, + { + "name": "first_flag", + "enclosing_namespace_id": null, + "return_type": "Flag", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 31 + } + }, + { + "name": "second_flag", + "enclosing_namespace_id": null, + "return_type": "Flag", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 32 + } + }, + { + "name": "operator&&", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [ + { + "name": "", + "param_type": "Flag", + "is_variadic": false + }, + { + "name": "", + "param_type": "Flag", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 34 + } + }, + { + "name": "guard_associativity", + "enclosing_namespace_id": "flow", + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 37 + } + } + ], "functions": [ { "id": { - "name": "guard_associativity", "scope": { "Namespace": [ "flow" ] - } + }, + "name": "guard_associativity" }, "kind": "Free", "return_type": { @@ -15,6 +209,7 @@ }, "body": [ { + "type": "branch", "cases": [ { "guard": { @@ -51,12 +246,12 @@ }, "body": [ { + "type": "call", "target": "handle_and", "source_location": { "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", "line": 40 - }, - "type": "call" + } } ], "source_location": { @@ -64,10 +259,10 @@ "line": 39 } } - ], - "type": "branch" + ] }, { + "type": "branch", "cases": [ { "guard": { @@ -109,12 +304,12 @@ }, "body": [ { + "type": "call", "target": "handle_or", "source_location": { "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", "line": 45 - }, - "type": "call" + } } ], "source_location": { @@ -122,10 +317,10 @@ "line": 44 } } - ], - "type": "branch" + ] }, { + "type": "branch", "cases": [ { "guard": { @@ -167,12 +362,12 @@ }, "body": [ { + "type": "call", "target": "handle_mixed", "source_location": { "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", "line": 50 - }, - "type": "call" + } } ], "source_location": { @@ -180,10 +375,10 @@ "line": 49 } } - ], - "type": "branch" + ] }, { + "type": "branch", "cases": [ { "guard": { @@ -211,12 +406,12 @@ }, "body": [ { + "type": "call", "target": "handle_alternative", "source_location": { "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", "line": 55 - }, - "type": "call" + } } ], "source_location": { @@ -224,10 +419,10 @@ "line": 54 } } - ], - "type": "branch" + ] }, { + "type": "branch", "cases": [ { "guard": { @@ -240,12 +435,12 @@ }, "body": [ { + "type": "call", "target": "handle_template_operand", "source_location": { "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", "line": 60 - }, - "type": "call" + } } ], "source_location": { @@ -253,10 +448,10 @@ "line": 59 } } - ], - "type": "branch" + ] }, { + "type": "branch", "cases": [ { "guard": { @@ -269,12 +464,12 @@ }, "body": [ { + "type": "call", "target": "handle_operator_function", "source_location": { "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", "line": 65 - }, - "type": "call" + } } ], "source_location": { @@ -282,29 +477,9 @@ "line": 64 } } - ], - "type": "branch" + ] } ] } - ], - "types": { - "Flag": { - "id": "Flag", - "name": "Flag", - "enclosing_namespace_id": null, - "stereotypes": [], - "entity_type": "Struct", - "type_aliases": [], - "variables": [], - "methods": [], - "template_parameters": null, - "enum_literals": [], - "relationships": [], - "source_location": { - "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", - "line": 30 - } - } - } -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/header_inline_function_dedup/expected.json b/cpp/libclang/integration_test/function_cases/header_inline_function_dedup/expected.json index abd289ab..53e65328 100644 --- a/cpp/libclang/integration_test/function_cases/header_inline_function_dedup/expected.json +++ b/cpp/libclang/integration_test/function_cases/header_inline_function_dedup/expected.json @@ -1,4 +1,39 @@ { + "free_function_declarations": [ + { + "name": "shared", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_function_dedup/inline_function.h", + "line": 16 + } + }, + { + "name": "first", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_function_dedup/first.cpp", + "line": 16 + } + }, + { + "name": "second", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_function_dedup/second.cpp", + "line": 16 + } + } + ], "functions": [ { "id": { diff --git a/cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/expected.json b/cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/expected.json index e2d811ae..4cccc982 100644 --- a/cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/expected.json +++ b/cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/expected.json @@ -1,13 +1,80 @@ { + "types": { + "util::Widget": { + "id": "util::Widget", + "name": "Widget", + "enclosing_namespace_id": "util", + "stereotypes": [], + "entity_type": "Class", + "type_aliases": [], + "variables": [], + "methods": [ + { + "name": "compute", + "return_type": "int", + "visibility": "public", + "parameters": [], + "template_parameters": null, + "modifiers": [], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/inline_member.h", + "line": 21 + } + } + ], + "template_parameters": null, + "enum_literals": [], + "relationships": [], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/inline_member.h", + "line": 19 + } + } + }, + "free_function_declarations": [ + { + "name": "ping", + "enclosing_namespace_id": "util", + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/inline_member.h", + "line": 17 + } + }, + { + "name": "first", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/first.cpp", + "line": 16 + } + }, + { + "name": "second", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/second.cpp", + "line": 16 + } + } + ], "functions": [ { "id": { - "name": "ping", "scope": { "Namespace": [ "util" ] - } + }, + "name": "ping" }, "kind": "Free", "return_type": { @@ -17,7 +84,6 @@ }, { "id": { - "name": "compute", "scope": { "Type": { "namespace": [ @@ -27,7 +93,8 @@ "Widget" ] } - } + }, + "name": "compute" }, "kind": "Method", "return_type": { @@ -46,8 +113,8 @@ }, { "id": { - "name": "first", - "scope": "Global" + "scope": "Global", + "name": "first" }, "kind": "Free", "return_type": { @@ -74,8 +141,8 @@ }, { "id": { - "name": "second", - "scope": "Global" + "scope": "Global", + "name": "second" }, "kind": "Free", "return_type": { @@ -100,37 +167,5 @@ } ] } - ], - "types": { - "util::Widget": { - "id": "util::Widget", - "name": "Widget", - "enclosing_namespace_id": "util", - "entity_type": "Class", - "enum_literals": [], - "methods": [ - { - "name": "compute", - "return_type": "int", - "parameters": [], - "modifiers": [], - "template_parameters": null, - "visibility": "public", - "source_location": { - "file": "cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/inline_member.h", - "line": 21 - } - } - ], - "relationships": [], - "stereotypes": [], - "template_parameters": null, - "type_aliases": [], - "variables": [], - "source_location": { - "file": "cpp/libclang/integration_test/function_cases/header_inline_member_function_dedup/inline_member.h", - "line": 19 - } - } - } -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/header_template_function_dedup/expected.json b/cpp/libclang/integration_test/function_cases/header_template_function_dedup/expected.json index 95d8ff5d..9793ab48 100644 --- a/cpp/libclang/integration_test/function_cases/header_template_function_dedup/expected.json +++ b/cpp/libclang/integration_test/function_cases/header_template_function_dedup/expected.json @@ -1,13 +1,62 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "identity", + "enclosing_namespace_id": "utility", + "return_type": "T", + "parameters": [ + { + "name": "value", + "param_type": "T", + "is_variadic": false + } + ], + "template_parameters": [ + { + "Type": { + "name": "T", + "is_pack": false + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_template_function_dedup/template.h", + "line": 18 + } + }, + { + "name": "first", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_template_function_dedup/first.cpp", + "line": 16 + } + }, + { + "name": "second", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/header_template_function_dedup/second.cpp", + "line": 16 + } + } + ], "functions": [ { "id": { - "name": "identity", "scope": { "Namespace": [ "utility" ] - } + }, + "name": "identity" }, "kind": "Free", "return_type": { @@ -17,8 +66,8 @@ }, { "id": { - "name": "first", - "scope": "Global" + "scope": "Global", + "name": "first" }, "kind": "Free", "return_type": { @@ -30,15 +79,15 @@ "target": "utility::identity", "source_location": { "file": "cpp/libclang/integration_test/function_cases/header_template_function_dedup/first.cpp", - "line": 17 + "line": 17 } } ] }, { "id": { - "name": "second", - "scope": "Global" + "scope": "Global", + "name": "second" }, "kind": "Free", "return_type": { @@ -50,11 +99,10 @@ "target": "utility::identity", "source_location": { "file": "cpp/libclang/integration_test/function_cases/header_template_function_dedup/second.cpp", - "line": 17 + "line": 17 } } ] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json b/cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json index 84bf6a58..59f6f2c6 100644 --- a/cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json +++ b/cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json @@ -1,13 +1,71 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "is_ready", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 14 + } + }, + { + "name": "is_allowed", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 15 + } + }, + { + "name": "has_permission", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 16 + } + }, + { + "name": "handle_complex", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 17 + } + }, + { + "name": "complex_condition", + "enclosing_namespace_id": "flow", + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 20 + } + } + ], "functions": [ { "id": { - "name": "complex_condition", "scope": { "Namespace": [ "flow" ] - } + }, + "name": "complex_condition" }, "kind": "Free", "return_type": { @@ -15,6 +73,7 @@ }, "body": [ { + "type": "branch", "cases": [ { "guard": { @@ -26,7 +85,7 @@ "text": "is_ready()", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", - "line": 21 + "line": 21 } }, { @@ -38,7 +97,7 @@ "text": "is_allowed()", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", - "line": 21 + "line": 21 } }, { @@ -49,7 +108,7 @@ "text": "has_permission()", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", - "line": 21 + "line": 21 } } } @@ -59,12 +118,12 @@ }, "body": [ { + "type": "call", "target": "handle_complex", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", "line": 22 - }, - "type": "call" + } } ], "source_location": { @@ -72,11 +131,9 @@ "line": 21 } } - ], - "type": "branch" + ] } ] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/if_else_chain/expected.json b/cpp/libclang/integration_test/function_cases/if_else_chain/expected.json index bee3509a..8848e5af 100644 --- a/cpp/libclang/integration_test/function_cases/if_else_chain/expected.json +++ b/cpp/libclang/integration_test/function_cases/if_else_chain/expected.json @@ -1,13 +1,77 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "is_ready", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 14 + } + }, + { + "name": "handle_ready", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 15 + } + }, + { + "name": "handle_retry", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 16 + } + }, + { + "name": "handle_failure", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 17 + } + }, + { + "name": "evaluate", + "enclosing_namespace_id": "flow", + "return_type": "void", + "parameters": [ + { + "name": "retry", + "param_type": "bool", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 22 + } + } + ], "functions": [ { "id": { - "name": "evaluate", "scope": { "Namespace": [ "flow" ] - } + }, + "name": "evaluate" }, "kind": "Free", "return_type": { @@ -15,6 +79,7 @@ }, "body": [ { + "type": "branch", "cases": [ { "guard": { @@ -28,12 +93,12 @@ }, "body": [ { + "type": "call", "target": "handle_ready", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", "line": 26 - }, - "type": "call" + } } ], "source_location": { @@ -52,12 +117,12 @@ }, "body": [ { + "type": "call", "target": "handle_retry", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", "line": 30 - }, - "type": "call" + } } ], "source_location": { @@ -69,12 +134,12 @@ "guard": null, "body": [ { + "type": "call", "target": "handle_failure", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", "line": 34 - }, - "type": "call" + } } ], "source_location": { @@ -82,11 +147,9 @@ "line": 33 } } - ], - "type": "branch" + ] } ] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json index 2f0a756b..05c5a329 100644 --- a/cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json +++ b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json @@ -1,45 +1,91 @@ { - "types": {}, - "functions": [ - { - "id": { - "scope": { - "Namespace": [ - "flow" - ] - }, - "name": "if_initializer_fallback" - }, - "kind": "Free", - "return_type": { - "Builtin": "void" - }, - "body": [ - { - "type": "call", - "target": "initialize", - "source_location": { - "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", - "line": 20 - } - }, - { - "type": "call", - "target": "handle_ready", - "source_location": { - "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", - "line": 21 - } - }, - { - "type": "call", - "target": "handle_failure", - "source_location": { - "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", - "line": 23 - } - } - ] - } - ] -} + "types": {}, + "free_function_declarations": [ + { + "name": "initialize", + "enclosing_namespace_id": null, + "return_type": "bool", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 14 + } + }, + { + "name": "handle_ready", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 15 + } + }, + { + "name": "handle_failure", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 16 + } + }, + { + "name": "if_initializer_fallback", + "enclosing_namespace_id": "flow", + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 19 + } + } + ], + "functions": [ + { + "id": { + "scope": { + "Namespace": [ + "flow" + ] + }, + "name": "if_initializer_fallback" + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "type": "call", + "target": "initialize", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 20 + } + }, + { + "type": "call", + "target": "handle_ready", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 21 + } + }, + { + "type": "call", + "target": "handle_failure", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 23 + } + } + ] + } + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/if_without_braces/expected.json b/cpp/libclang/integration_test/function_cases/if_without_braces/expected.json index d8f1e993..852d6cc2 100644 --- a/cpp/libclang/integration_test/function_cases/if_without_braces/expected.json +++ b/cpp/libclang/integration_test/function_cases/if_without_braces/expected.json @@ -1,13 +1,55 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "handle_unbraced", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 14 + } + }, + { + "name": "handle_failure", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 15 + } + }, + { + "name": "without_braces", + "enclosing_namespace_id": "flow", + "return_type": "void", + "parameters": [ + { + "name": "enabled", + "param_type": "bool", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 18 + } + } + ], "functions": [ { "id": { - "name": "without_braces", "scope": { "Namespace": [ "flow" ] - } + }, + "name": "without_braces" }, "kind": "Free", "return_type": { @@ -15,6 +57,7 @@ }, "body": [ { + "type": "branch", "cases": [ { "guard": { @@ -22,17 +65,17 @@ "text": "enabled", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", - "line": 19 + "line": 19 } }, "body": [ { + "type": "call", "target": "handle_unbraced", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", - "line": 20 - }, - "type": "call" + "line": 20 + } } ], "source_location": { @@ -44,12 +87,12 @@ "guard": null, "body": [ { + "type": "call", "target": "handle_failure", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", "line": 22 - }, - "type": "call" + } } ], "source_location": { @@ -57,11 +100,9 @@ "line": 22 } } - ], - "type": "branch" + ] } ] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/if_without_else/expected.json b/cpp/libclang/integration_test/function_cases/if_without_else/expected.json index 23186f8a..fe90c4d7 100644 --- a/cpp/libclang/integration_test/function_cases/if_without_else/expected.json +++ b/cpp/libclang/integration_test/function_cases/if_without_else/expected.json @@ -1,13 +1,44 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "handle_no_else", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp", + "line": 14 + } + }, + { + "name": "without_else", + "enclosing_namespace_id": "flow", + "return_type": "void", + "parameters": [ + { + "name": "enabled", + "param_type": "bool", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp", + "line": 17 + } + } + ], "functions": [ { "id": { - "name": "without_else", "scope": { "Namespace": [ "flow" ] - } + }, + "name": "without_else" }, "kind": "Free", "return_type": { @@ -15,6 +46,7 @@ }, "body": [ { + "type": "branch", "cases": [ { "guard": { @@ -22,17 +54,17 @@ "text": "enabled", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp", - "line": 18 + "line": 18 } }, "body": [ { + "type": "call", "target": "handle_no_else", "source_location": { "file": "cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp", - "line": 19 - }, - "type": "call" + "line": 19 + } } ], "source_location": { @@ -40,11 +72,9 @@ "line": 18 } } - ], - "type": "branch" + ] } ] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/inherited_class_method_template/expected.json b/cpp/libclang/integration_test/function_cases/inherited_class_method_template/expected.json index 52564e7e..c163d048 100644 --- a/cpp/libclang/integration_test/function_cases/inherited_class_method_template/expected.json +++ b/cpp/libclang/integration_test/function_cases/inherited_class_method_template/expected.json @@ -86,6 +86,19 @@ } } }, + "free_function_declarations": [ + { + "name": "notify", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/inherited_class_method_template/base.h", + "line": 16 + } + } + ], "functions": [ { "id": { diff --git a/cpp/libclang/integration_test/function_cases/nested_if/expected.json b/cpp/libclang/integration_test/function_cases/nested_if/expected.json index 33a7c827..7b9f51ca 100644 --- a/cpp/libclang/integration_test/function_cases/nested_if/expected.json +++ b/cpp/libclang/integration_test/function_cases/nested_if/expected.json @@ -1,13 +1,60 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "handle_outer_else", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 14 + } + }, + { + "name": "handle_nested", + "enclosing_namespace_id": null, + "return_type": "void", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 15 + } + }, + { + "name": "nested_if", + "enclosing_namespace_id": "flow", + "return_type": "void", + "parameters": [ + { + "name": "outer", + "param_type": "bool", + "is_variadic": false + }, + { + "name": "inner", + "param_type": "bool", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 18 + } + } + ], "functions": [ { "id": { - "name": "nested_if", "scope": { "Namespace": [ "flow" ] - } + }, + "name": "nested_if" }, "kind": "Free", "return_type": { @@ -15,6 +62,7 @@ }, "body": [ { + "type": "branch", "cases": [ { "guard": { @@ -22,11 +70,12 @@ "text": "outer", "source_location": { "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", - "line": 19 + "line": 19 } }, "body": [ { + "type": "branch", "cases": [ { "guard": { @@ -39,12 +88,12 @@ }, "body": [ { + "type": "call", "target": "handle_nested", "source_location": { "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", "line": 21 - }, - "type": "call" + } } ], "source_location": { @@ -52,8 +101,7 @@ "line": 20 } } - ], - "type": "branch" + ] } ], "source_location": { @@ -65,12 +113,12 @@ "guard": null, "body": [ { + "type": "call", "target": "handle_outer_else", "source_location": { "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", - "line": 24 - }, - "type": "call" + "line": 24 + } } ], "source_location": { @@ -78,11 +126,9 @@ "line": 23 } } - ], - "type": "branch" + ] } ] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/BUILD b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/BUILD new file mode 100644 index 00000000..8695623f --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/BUILD @@ -0,0 +1,26 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("//cpp/libclang/integration_test:test_rules.bzl", "cpp_parser_integration_test") + +cc_library( + name = "out_of_line_method_dedup", + srcs = ["functions.cpp"], + hdrs = ["functions.hpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_out_of_line_method_dedup", + expected_output = ["expected.json"], + target = ":out_of_line_method_dedup", +) diff --git a/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/expected.json b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/expected.json new file mode 100644 index 00000000..509ce8af --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/expected.json @@ -0,0 +1,54 @@ +{ + "functions": [ + { + "id": { + "scope": { + "Type": { + "namespace": [], + "type_path": [ + "A" + ] + } + }, + "name": "run" + }, + "kind": "Method", + "return_type": { + "Builtin": "void" + }, + "body": [] + } + ], + "types": { + "A": { + "id": "A", + "name": "A", + "enclosing_namespace_id": null, + "entity_type": "Struct", + "enum_literals": [], + "methods": [ + { + "name": "run", + "return_type": "void", + "visibility": "public", + "parameters": [], + "template_parameters": null, + "modifiers": [], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.hpp", + "line": 17 + } + } + ], + "relationships": [], + "stereotypes": [], + "template_parameters": null, + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.hpp", + "line": 16 + } + } + } +} diff --git a/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.cpp b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.cpp new file mode 100644 index 00000000..bd3ab783 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.cpp @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "functions.hpp" + +void A::run() {} diff --git a/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.hpp b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.hpp new file mode 100644 index 00000000..9814c1e1 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/functions.hpp @@ -0,0 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#pragma once + +struct A { + void run(); +}; diff --git a/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/run_test.rs b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/run_test.rs new file mode 100644 index 00000000..ed482d1d --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/out_of_line_method_dedup/run_test.rs @@ -0,0 +1,19 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use test_framework::run_parser_case; + +#[test] +fn test_out_of_line_method_dedup() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/standard_library_header_filter/expected.json b/cpp/libclang/integration_test/function_cases/standard_library_header_filter/expected.json index 4e8ec0b9..893476f6 100644 --- a/cpp/libclang/integration_test/function_cases/standard_library_header_filter/expected.json +++ b/cpp/libclang/integration_test/function_cases/standard_library_header_filter/expected.json @@ -1,9 +1,29 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "local_function", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [ + { + "name": "value", + "param_type": "int", + "is_variadic": false + } + ], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/standard_library_header_filter/functions.cpp", + "line": 16 + } + } + ], "functions": [ { "id": { - "name": "local_function", - "scope": "Global" + "scope": "Global", + "name": "local_function" }, "kind": "Free", "return_type": { @@ -11,6 +31,5 @@ }, "body": [] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/integration_test/function_cases/template_declaration_without_definition/expected.json b/cpp/libclang/integration_test/function_cases/template_declaration_without_definition/expected.json index 27b800f2..dcf519bf 100644 --- a/cpp/libclang/integration_test/function_cases/template_declaration_without_definition/expected.json +++ b/cpp/libclang/integration_test/function_cases/template_declaration_without_definition/expected.json @@ -1,9 +1,47 @@ { + "types": {}, + "free_function_declarations": [ + { + "name": "declared_only", + "enclosing_namespace_id": "utility", + "return_type": "T", + "parameters": [ + { + "name": "value", + "param_type": "T", + "is_variadic": false + } + ], + "template_parameters": [ + { + "Type": { + "name": "T", + "is_pack": false + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/template_declaration_without_definition/declaration_only.h", + "line": 21 + } + }, + { + "name": "run", + "enclosing_namespace_id": null, + "return_type": "int", + "parameters": [], + "template_parameters": null, + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/template_declaration_without_definition/functions.cpp", + "line": 16 + } + } + ], "functions": [ { "id": { - "name": "run", - "scope": "Global" + "scope": "Global", + "name": "run" }, "kind": "Free", "return_type": { @@ -20,6 +58,5 @@ } ] } - ], - "types": {} -} + ] +} \ No newline at end of file diff --git a/cpp/libclang/src/main.rs b/cpp/libclang/src/main.rs index 3c060a1b..7ca84d3b 100644 --- a/cpp/libclang/src/main.rs +++ b/cpp/libclang/src/main.rs @@ -18,13 +18,13 @@ use std::collections::{BTreeMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; -use class_diagram::{ClassDiagram, SimpleEntity}; +use class_diagram::{ClassDiagram, FreeFunctionDecl, SimpleEntity}; use class_serializer::ClassSerializer; use utils::{render_entity_tree, write_debug_json, write_entity_tree, write_fbs_output}; use visit_tu::{ - is_external_dependency_path, FunctionDef, FunctionDefinitionKey, SourceFileCache, VisitContext, - Visitor, + is_external_dependency_path, CallableIdentityKey, EntityMapExt, FunctionDef, SourceEntityKey, + SourceFileCache, VisitContext, Visitor, }; #[derive(ClapParser, Debug)] @@ -53,13 +53,16 @@ struct Args { #[derive(Default)] struct ParseOutputs { types: BTreeMap, + free_function_declarations: Vec, functions: Vec, } #[derive(Default)] struct ParseState { source_files: SourceFileCache, - seen_function_definitions: HashSet, + seen_free_function_declarations: HashSet, + seen_method_declarations: HashSet, + seen_function_definitions: HashSet, } impl ParseOutputs { @@ -72,9 +75,20 @@ impl ParseOutputs { for (type_name, entity) in ctx.types { debug!("Type {}:\n{:#?}", type_name, entity); - self.types.insert(type_name, entity); + self.types.insert_or_merge_type(type_name, entity); } - + self.free_function_declarations + .extend( + ctx.free_function_declarations + .into_iter() + .map(|declaration| { + debug!( + "Free function declaration: {}", + declaration.declaration.qualified_name() + ); + declaration.declaration + }), + ); self.functions.extend( ctx.functions .into_iter() @@ -165,6 +179,8 @@ fn parse_file( let mut visitor = Visitor::new( &mut ctx, &mut state.source_files, + &mut state.seen_free_function_declarations, + &mut state.seen_method_declarations, &mut state.seen_function_definitions, ); visitor.visit(entity); @@ -179,11 +195,13 @@ fn parse_file( fn serialize_class_diagram( output_path: &Path, entities: BTreeMap, + free_functions: Vec, ) -> Result<(), std::io::Error> { let entities: Vec<_> = entities.into_values().collect(); let class_diagram = ClassDiagram { name: String::new(), // no name for c++ side entities, + free_functions, }; let output_fbs = ClassSerializer::serialize(&class_diagram); @@ -234,10 +252,20 @@ fn main() -> Result<(), Box> { } if let Some(debug_json_output) = &command_line_args.debug_json_output { - write_debug_json(debug_json_output, &outputs.types, &outputs.functions)?; + write_debug_json( + debug_json_output, + &outputs.types, + (!outputs.free_function_declarations.is_empty()) + .then_some(&outputs.free_function_declarations), + &outputs.functions, + )?; } - serialize_class_diagram(&command_line_args.class_fbs_output, outputs.types)?; + serialize_class_diagram( + &command_line_args.class_fbs_output, + outputs.types, + outputs.free_function_declarations, + )?; Ok(()) } diff --git a/cpp/libclang/src/semantics/src/callable.rs b/cpp/libclang/src/semantics/src/callable.rs index 386fa7ff..f0b4cbda 100644 --- a/cpp/libclang/src/semantics/src/callable.rs +++ b/cpp/libclang/src/semantics/src/callable.rs @@ -59,7 +59,7 @@ pub enum FunctionKind { Conversion, } -/// Stable semantic identity of a C++ callable. +/// Lightweight semantic identity of a C++ callable within this model. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct FunctionId { pub scope: Scope, diff --git a/cpp/libclang/src/utils/write.rs b/cpp/libclang/src/utils/write.rs index b451670f..14cc7a49 100644 --- a/cpp/libclang/src/utils/write.rs +++ b/cpp/libclang/src/utils/write.rs @@ -42,17 +42,25 @@ fn write_entity_tree_inner(path: &Path, entity_tree: &str) -> std::io::Result<() file_out.flush() } -pub fn write_debug_json( +pub fn write_debug_json( output_path: &Path, types: &T, - functions: &U, + free_function_declarations: Option<&U>, + functions: &V, ) -> Result<(), Box> where T: Serialize, U: Serialize, + V: Serialize, { let mut debug_json = serde_json::Map::new(); debug_json.insert("types".to_owned(), serde_json::to_value(types)?); + if let Some(free_function_declarations) = free_function_declarations { + debug_json.insert( + "free_function_declarations".to_owned(), + serde_json::to_value(free_function_declarations)?, + ); + } debug_json.insert("functions".to_owned(), serde_json::to_value(functions)?); let output_json = serde_json::to_string_pretty(&debug_json)?; diff --git a/cpp/libclang/src/visitor/BUILD b/cpp/libclang/src/visitor/BUILD index 64f96fa0..6af50ef6 100644 --- a/cpp/libclang/src/visitor/BUILD +++ b/cpp/libclang/src/visitor/BUILD @@ -15,6 +15,7 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") rust_library( name = "visit_tu", srcs = [ + "src/callable_declaration.rs", "src/clang_adapter/mod.rs", "src/clang_adapter/scope.rs", "src/clang_adapter/source_filter.rs", @@ -22,6 +23,7 @@ rust_library( "src/class_relationship_resolver.rs", "src/class_visitor.rs", "src/context.rs", + "src/context_ext.rs", "src/enum_visitor.rs", "src/function_visitor.rs", "src/lib.rs", diff --git a/cpp/libclang/src/visitor/src/callable_declaration.rs b/cpp/libclang/src/visitor/src/callable_declaration.rs new file mode 100644 index 00000000..66a80522 --- /dev/null +++ b/cpp/libclang/src/visitor/src/callable_declaration.rs @@ -0,0 +1,124 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use clang::{Entity, EntityKind}; +use class_diagram::{FunctionArgument, TemplateParameter}; + +use crate::types::renderer::render_type_for_display; +use crate::types::resolver::resolve_type; + +/// Returns callable parameters, including the fallback required for template cursors. +/// +/// Normally libclang provides the parameter list via `Entity::get_arguments()`. +/// However, for some cursor kinds (e.g. `FunctionTemplate`) or certain libclang +/// versions, `get_arguments()` may return `None` even though the AST still +/// contains `ParmDecl` child cursors. +pub(crate) fn callable_arguments<'tu>(entity: &Entity<'tu>) -> Vec> { + // fall back to collecting all direct `ParmDecl` children from + // the cursor to recover the parameter list. + entity.get_arguments().unwrap_or_else(|| { + entity + .get_children() + .into_iter() + .filter(|child| child.get_kind() == EntityKind::ParmDecl) + .collect() + }) +} + +pub(crate) fn parse_function_parameters(entity: &Entity) -> Vec { + let mut parameters: Vec = callable_arguments(entity) + .into_iter() + .map(|argument| { + let raw_param_type = argument + .get_type() + .map(|ty| ty.get_display_name()) + .unwrap_or_default(); + + FunctionArgument { + name: argument.get_name().unwrap_or_default(), + param_type: Some(normalize_pack_expansion_type(&raw_param_type)), + is_variadic: false, + is_pack_expansion: raw_param_type.contains("..."), + } + }) + .collect(); + + if entity.get_type().is_some_and(|ty| ty.is_variadic()) { + parameters.push(FunctionArgument { + name: String::new(), + param_type: None, + is_variadic: true, + is_pack_expansion: false, + }); + } + + parameters +} + +pub(crate) fn parse_callable_return_type(entity: &Entity) -> Option { + entity.get_result_type().map(|return_type| { + let resolved_type = resolve_type(&return_type); + render_type_for_display(&return_type, &resolved_type) + }) +} + +pub(crate) fn parse_template_parameters(entity: &Entity) -> Option> { + let parameters = entity + .get_children() + .into_iter() + .enumerate() + .filter_map(|(index, child)| match child.get_kind() { + // template → "name: Foo, is_pack: False" + // template -> "name: T0, is_pack: False", "name: T1, is_pack: False" + // template -> "name: Foo, is_pack: True" + EntityKind::TemplateTypeParameter => Some(TemplateParameter::Type { + name: child.get_name().unwrap_or_else(|| format!("T{index}")), + is_pack: is_template_parameter_pack(&child), + }), + // template → "name: N, value_type: int" + EntityKind::NonTypeTemplateParameter => Some(TemplateParameter::NonType { + name: child.get_name().unwrap_or_default(), + value_type: child + .get_type() + .map(|ty| ty.get_display_name()) + .unwrap_or_default(), + is_pack: is_template_parameter_pack(&child), + }), + // template class C> → "name: C, parameters: [...], is_pack: False" + EntityKind::TemplateTemplateParameter => Some(TemplateParameter::Template { + name: child.get_name().unwrap_or_else(|| format!("T{index}")), + parameters: parse_template_parameters(&child).unwrap_or_default(), + is_pack: is_template_parameter_pack(&child), + }), + _ => None, + }) + .collect::>(); + + (!parameters.is_empty()).then_some(parameters) +} + +fn normalize_pack_expansion_type(param_type: &str) -> String { + param_type.replace("...", "").trim().to_string() +} + +fn is_template_parameter_pack(entity: &Entity) -> bool { + entity.get_range().is_some_and(|range| { + range + .tokenize() + .iter() + .any(|token| token.get_spelling() == "...") + }) || entity + .get_display_name() + .as_deref() + .is_some_and(|display_name| display_name.contains("...")) +} diff --git a/cpp/libclang/src/visitor/src/class_relationship_resolver.rs b/cpp/libclang/src/visitor/src/class_relationship_resolver.rs index eda98b9b..8e79f0d5 100644 --- a/cpp/libclang/src/visitor/src/class_relationship_resolver.rs +++ b/cpp/libclang/src/visitor/src/class_relationship_resolver.rs @@ -24,7 +24,7 @@ pub(crate) fn resolve_relationships(ctx: &mut VisitContext) { let builders = std::mem::take(&mut ctx.parsed_class_info); let known_type_ids: HashSet = ctx.types.keys().cloned().collect(); - for builder in builders { + for builder in builders.into_values() { build_relationships_for_class(ctx, &builder); infer_relationships_from_builder(ctx, &builder, &known_type_ids); } @@ -267,21 +267,25 @@ mod tests { }, ); - ctx.parsed_class_info.push(ParsedClassInfo { - id: "Car".to_string(), - base_classes: vec![], - variable_types: vec![ParsedVariableType { - name: "engine".to_string(), - resolved_type: ResolvedType::UserDefined("Engine".to_string()), - source_location: SourceLocation::new(source_file, 5), - }], - method_types: vec![ParsedMethodType { - name: "buildEngine".to_string(), - return_type: ResolvedType::UserDefined("Engine".to_string()), - parameter_types: vec![], - source_location: SourceLocation::new(source_file, 6), - }], - }); + ctx.parsed_class_info.insert( + "Car".to_string(), + ParsedClassInfo { + id: "Car".to_string(), + base_classes: vec![], + variable_types: vec![ParsedVariableType { + name: "engine".to_string(), + resolved_type: ResolvedType::UserDefined("Engine".to_string()), + source_location: SourceLocation::new(source_file, 5), + }], + method_types: vec![ParsedMethodType { + name: "buildEngine".to_string(), + return_type: ResolvedType::UserDefined("Engine".to_string()), + parameter_types: vec![], + source_location: SourceLocation::new(source_file, 6), + }], + ..Default::default() + }, + ); resolve_relationships(&mut ctx); @@ -349,27 +353,31 @@ mod tests { ..Default::default() }, ); - ctx.parsed_class_info.push(ParsedClassInfo { - id: "amp::detail::is_maplike_container".to_string(), - base_classes: vec![ - // Unresolvable dependent expression — must be skipped, not panic. - ParsedBaseClass { - resolved_type: ResolvedType::Dependent( - "decltype(is_maplike_container_impl(std::declval()))".to_string(), - ), - source_location: SourceLocation::new(source_file, 5), - }, - // A normal, resolvable base class alongside the dependent one. - ParsedBaseClass { - resolved_type: ResolvedType::UserDefined( - "amp::detail::is_container_base".to_string(), - ), - source_location: SourceLocation::new(source_file, 5), - }, - ], - variable_types: vec![], - method_types: vec![], - }); + ctx.parsed_class_info.insert( + "amp::detail::is_maplike_container".to_string(), + ParsedClassInfo { + id: "amp::detail::is_maplike_container".to_string(), + base_classes: vec![ + // Unresolvable dependent expression — must be skipped, not panic. + ParsedBaseClass { + resolved_type: ResolvedType::Dependent( + "decltype(is_maplike_container_impl(std::declval()))".to_string(), + ), + source_location: SourceLocation::new(source_file, 5), + }, + // A normal, resolvable base class alongside the dependent one. + ParsedBaseClass { + resolved_type: ResolvedType::UserDefined( + "amp::detail::is_container_base".to_string(), + ), + source_location: SourceLocation::new(source_file, 5), + }, + ], + variable_types: vec![], + method_types: vec![], + ..Default::default() + }, + ); // Must not panic. resolve_relationships(&mut ctx); @@ -419,16 +427,20 @@ mod tests { ..Default::default() }, ); - ctx.parsed_class_info.push(ParsedClassInfo { - id: "Derived".to_string(), - base_classes: vec![ParsedBaseClass { - // Not `Dependent`: an unexpected, unresolvable base type. - resolved_type: ResolvedType::Unknown("SomeWeirdType".to_string()), - source_location: SourceLocation::new(source_file, 1), - }], - variable_types: vec![], - method_types: vec![], - }); + ctx.parsed_class_info.insert( + "Derived".to_string(), + ParsedClassInfo { + id: "Derived".to_string(), + base_classes: vec![ParsedBaseClass { + // Not `Dependent`: an unexpected, unresolvable base type. + resolved_type: ResolvedType::Unknown("SomeWeirdType".to_string()), + source_location: SourceLocation::new(source_file, 1), + }], + variable_types: vec![], + method_types: vec![], + ..Default::default() + }, + ); // Must not panic. resolve_relationships(&mut ctx); diff --git a/cpp/libclang/src/visitor/src/class_visitor.rs b/cpp/libclang/src/visitor/src/class_visitor.rs index 75e7b8e3..3fa2b772 100644 --- a/cpp/libclang/src/visitor/src/class_visitor.rs +++ b/cpp/libclang/src/visitor/src/class_visitor.rs @@ -11,18 +11,17 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use clang::{Entity, EntityKind, ExceptionSpecification}; +use clang::{Entity, EntityKind}; use class_diagram::{ - EntityType, FunctionArgument, MemberVariable, Method, MethodModifier, SimpleEntity, - TemplateParameter, TypeAlias, Visibility, + EntityType, MemberVariable, Method, MethodModifier, SimpleEntity, TypeAlias, Visibility, }; -use cpp_semantics::ResolvedType; +use crate::callable_declaration::parse_template_parameters; use crate::clang_adapter::scope::{namespace_id, semantic_parent_id}; use crate::clang_adapter::source_location::parse_source_location; use crate::context::{ - ParsedBaseClass, ParsedClassInfo, ParsedMethodType, ParsedVariableType, VisitContext, + ExtractedMethodDeclaration, ParsedBaseClass, ParsedClassInfo, ParsedVariableType, VisitContext, }; use crate::types::renderer::render_type_for_display; use crate::types::resolver::resolve_type; @@ -45,7 +44,7 @@ impl AstVisitor for ClassVisitor { Self::visit_class(&entity, semantic_parent.as_deref(), namespace.as_deref()) { class_entity.template_parameters = template_params; - ctx.parsed_class_info.push(builder); + ctx.parsed_class_info.insert(builder.id.clone(), builder); ctx.types.insert(class_entity.id.clone(), class_entity); } } @@ -58,6 +57,30 @@ impl ClassVisitor { crate::class_relationship_resolver::resolve_relationships(ctx); } + /// Adds a callable declaration to its owning class and preserves the + /// class-level metadata used by relationship inference. + pub(crate) fn add_method_declaration( + ctx: &mut VisitContext, + declaration: ExtractedMethodDeclaration, + ) { + let (types, parsed_class_info) = (&mut ctx.types, &mut ctx.parsed_class_info); + let (Some(class), Some(builder)) = ( + types.get_mut(&declaration.class_id), + parsed_class_info.get_mut(&declaration.class_id), + ) else { + log::warn!( + "method '{}' has incompletely registered owning class '{}'; skipping declaration", + declaration.method.name, + declaration.class_id + ); + return; + }; + + update_entity_type_for_method(class, builder, &declaration.method); + class.methods.push(declaration.method); + builder.method_types.push(declaration.method_type); + } + fn visit_class( entity: &Entity, semantic_parent: Option<&str>, @@ -75,6 +98,8 @@ impl ClassVisitor { base_classes: vec![], variable_types: vec![], method_types: vec![], + has_abstract_methods: false, + has_concrete_methods: false, }; let mut class_entity = SimpleEntity { @@ -89,7 +114,9 @@ impl ClassVisitor { Self::visit_member(&child, &mut class_entity, &mut builder); } - class_entity.entity_type = infer_entity_type_from_members(entity.get_kind(), &class_entity); + if entity.get_kind() == EntityKind::StructDecl { + class_entity.entity_type = EntityType::Struct; + } class_entity.source_location = parse_source_location(entity); @@ -106,12 +133,6 @@ impl ClassVisitor { }); } } - EntityKind::Method | EntityKind::Constructor | EntityKind::Destructor => { - let parsed_method_type = collect_method_type(entity, builder); - if let Some(method) = parse_method(entity, &parsed_method_type) { - class.methods.push(method); - } - } EntityKind::FieldDecl | EntityKind::VarDecl => { let Some(parsed_variable_type) = collect_variable_type(entity) else { return; @@ -122,17 +143,6 @@ impl ClassVisitor { class.variables.push(variable); } } - EntityKind::FunctionTemplate => { - let template_params = parse_template_parameters(entity); - let parsed_method_type = collect_method_type(entity, builder); - - // In current libclang/clang-rs output, method templates are represented - // directly on the FunctionTemplate entity. - if let Some(mut method) = parse_method(entity, &parsed_method_type) { - method.template_parameters = template_params; - class.methods.push(method); - } - } // `using Alias = OriginalType;` -> TypeAliasDecl // `typedef OriginalType Alias;` -> TypedefDecl EntityKind::TypeAliasDecl | EntityKind::TypedefDecl => { @@ -180,45 +190,6 @@ fn collect_variable_type(entity: &Entity) -> Option { }) } -fn collect_method_type(entity: &Entity, builder: &mut ParsedClassInfo) -> ParsedMethodType { - let name = entity.get_name().unwrap_or_default(); - - let return_type = entity - .get_result_type() - .map(|t| resolve_type(&t)) - .unwrap_or_else(|| ResolvedType::Builtin("void".to_string())); - let parameter_types = method_arguments(entity) - .into_iter() - .filter_map(|arg| arg.get_type().map(|t| resolve_type(&t))) - .collect(); - - let parsed_method_type = ParsedMethodType { - name, - return_type, - parameter_types, - source_location: parse_source_location(entity), - }; - builder.method_types.push(parsed_method_type.clone()); - - parsed_method_type -} - -/// Normally libclang provides the parameter list via `Entity::get_arguments()`. -/// However, for some cursor kinds (e.g. `FunctionTemplate`) or certain libclang -/// versions, `get_arguments()` may return `None` even though the AST still -/// contains `ParmDecl` child cursors. -fn method_arguments<'tu>(entity: &Entity<'tu>) -> Vec> { - entity.get_arguments().unwrap_or_else(|| { - // fall back to collecting all direct `ParmDecl` children from - // the cursor to recover the parameter list. - entity - .get_children() - .into_iter() - .filter(|child| child.get_kind() == EntityKind::ParmDecl) - .collect() - }) -} - fn parse_type_alias(entity: &Entity) -> Option { let Some(alias) = entity.get_name() else { log::debug!("skipping type alias: entity has no name"); @@ -243,94 +214,6 @@ fn parse_type_alias(entity: &Entity) -> Option { }) } -fn parse_method(entity: &Entity, parsed_method_type: &ParsedMethodType) -> Option { - let kind = entity.get_kind(); - let name = entity.get_name()?; - let is_override_method = entity - .get_overridden_methods() - .map(|methods| !methods.is_empty()) - .unwrap_or(false); - - // Only the bare `noexcept` specifier is modeled (mirrors the PlantUML grammar, which has - // no support for the conditional `noexcept(expr)` form). Requiring `BasicNoexcept` filters - // out `noexcept(expr)`, but on its own it isn't enough: for an implicit/defaulted special - // member (e.g. `~Foo() = default;` with no written specifier at all), the compiler-computed - // specification also resolves to `BasicNoexcept` once evaluated -- and that evaluation is - // lazily triggered by unrelated code (e.g. a derived class use), making it unstable. So this - // also requires the literal `noexcept` token to appear in the declarator (the tokens up to - // the first `{` or `;`), which excludes both that case and `noexcept` written inside a - // lambda in the method body. - let has_noexcept_token = entity.get_range().is_some_and(|range| { - range - .tokenize() - .iter() - .take_while(|token| !matches!(token.get_spelling().as_str(), "{" | ";")) - .any(|token| token.get_spelling() == "noexcept") - }); - - let is_noexcept_method = has_noexcept_token - && matches!( - entity.get_exception_specification(), - Some(ExceptionSpecification::BasicNoexcept) - ); - - let return_type = if matches!(kind, EntityKind::Constructor | EntityKind::Destructor) { - None - } else { - entity - .get_result_type() - .map(|ret| render_type_for_display(&ret, &parsed_method_type.return_type)) - }; - - let mut parameters = Vec::new(); - let method_is_variadic = entity.get_type().map(|t| t.is_variadic()).unwrap_or(false); - - let args = method_arguments(entity); - - for arg in args { - let raw_param_type = arg - .get_type() - .map(|ty| ty.get_display_name()) - .unwrap_or_default(); - let is_pack_expansion = raw_param_type.contains("..."); - let param_type = normalize_pack_expansion_type(&raw_param_type); - - parameters.push(FunctionArgument { - name: arg.get_name().unwrap_or_default(), - param_type: Some(param_type), - is_variadic: false, - is_pack_expansion, - }); - } - - if method_is_variadic { - parameters.push(FunctionArgument { - name: String::new(), - param_type: None, - is_variadic: true, - is_pack_expansion: false, - }); - } - - Some(Method { - name, - return_type, - visibility: parse_visibility(entity), - parameters, - template_parameters: None, - modifiers: MethodModifier::from_conditions([ - (entity.is_static_method(), MethodModifier::Static), - (entity.is_virtual_method(), MethodModifier::Virtual), - (entity.is_pure_virtual_method(), MethodModifier::Abstract), - (is_override_method, MethodModifier::Override), - (is_noexcept_method, MethodModifier::Noexcept), - (kind == EntityKind::Constructor, MethodModifier::Constructor), - (kind == EntityKind::Destructor, MethodModifier::Destructor), - ]), - source_location: parse_source_location(entity), - }) -} - fn parse_variable( entity: &Entity, parsed_variable_type: &ParsedVariableType, @@ -346,76 +229,7 @@ fn parse_variable( }) } -fn parse_template_parameters(entity: &Entity) -> Option> { - let params: Vec = entity - .get_children() - .into_iter() - .enumerate() - .filter_map(|(idx, child)| match child.get_kind() { - EntityKind::TemplateTypeParameter => { - // template → "name: Foo, is_pack: False" - // template -> "name: T0, is_pack: False", "name: T1, is_pack: False" - // template -> "name: Foo, is_pack: True" - let name = child.get_name().unwrap_or_else(|| format!("T{idx}")); - - Some(TemplateParameter::Type { - name, - is_pack: is_template_parameter_pack(&child), - }) - } - EntityKind::NonTypeTemplateParameter => { - // template → "name: N, value_type: int" - let type_name = child - .get_type() - .map(|t| t.get_display_name()) - .unwrap_or_default(); - let name = child.get_name().unwrap_or_default(); - - Some(TemplateParameter::NonType { - name, - value_type: type_name, - is_pack: is_template_parameter_pack(&child), - }) - } - EntityKind::TemplateTemplateParameter => { - // template class C> → "name: C, parameters: [...], is_pack: False" - let parameters = parse_template_parameters(&child).unwrap_or_default(); - let name = child.get_name().unwrap_or_else(|| format!("T{idx}")); - - Some(TemplateParameter::Template { - name, - parameters, - is_pack: is_template_parameter_pack(&child), - }) - } - _ => None, - }) - .collect(); - - if params.is_empty() { - None - } else { - Some(params) - } -} - -fn normalize_pack_expansion_type(param_type: &str) -> String { - param_type.replace("...", "").trim().to_string() -} - -fn is_template_parameter_pack(entity: &Entity) -> bool { - entity.get_range().is_some_and(|range| { - range - .tokenize() - .iter() - .any(|token| token.get_spelling() == "...") - }) || entity - .get_display_name() - .as_deref() - .is_some_and(|display_name| display_name.contains("...")) -} - -fn parse_visibility(entity: &Entity) -> Visibility { +pub(crate) fn parse_visibility(entity: &Entity) -> Visibility { match entity.get_accessibility() { Some(clang::Accessibility::Public) => Visibility::Public, Some(clang::Accessibility::Private) => Visibility::Private, @@ -424,39 +238,44 @@ fn parse_visibility(entity: &Entity) -> Visibility { } } -fn infer_entity_type_from_members(kind: EntityKind, class: &SimpleEntity) -> EntityType { - if kind == EntityKind::StructDecl { - return EntityType::Struct; +fn update_entity_type_for_method( + class: &mut SimpleEntity, + builder: &mut ParsedClassInfo, + method: &Method, +) { + if class.entity_type == EntityType::Struct { + return; } - let has_data_members = !class.variables.is_empty(); - let mut has_abstract_methods = false; - let mut has_concrete_methods = false; - - for method in &class.methods { - let is_abstract = method - .modifiers - .iter() - .any(|m| matches!(m, MethodModifier::Abstract)); - let is_constructor_or_destructor = method - .modifiers - .iter() - .any(|m| matches!(m, MethodModifier::Constructor | MethodModifier::Destructor)); - - if is_abstract { - has_abstract_methods = true; - } else if !is_constructor_or_destructor { - has_concrete_methods = true; - } - } + update_method_flags(builder, method); - if has_abstract_methods { - if !has_concrete_methods && !has_data_members { - EntityType::Interface - } else { - EntityType::AbstractClass - } - } else { - EntityType::Class + class.entity_type = match ( + builder.has_abstract_methods, + builder.has_concrete_methods, + class.variables.is_empty(), + ) { + (true, false, true) => EntityType::Interface, + (true, _, _) => EntityType::AbstractClass, + _ => EntityType::Class, + }; +} + +fn update_method_flags(builder: &mut ParsedClassInfo, method: &Method) { + let is_abstract = method + .modifiers + .iter() + .any(|modifier| matches!(modifier, MethodModifier::Abstract)); + + let is_special_method = method.modifiers.iter().any(|modifier| { + matches!( + modifier, + MethodModifier::Constructor | MethodModifier::Destructor + ) + }); + + if is_abstract { + builder.has_abstract_methods = true; + } else if !is_special_method { + builder.has_concrete_methods = true; } } diff --git a/cpp/libclang/src/visitor/src/context.rs b/cpp/libclang/src/visitor/src/context.rs index 912cd868..c4777311 100644 --- a/cpp/libclang/src/visitor/src/context.rs +++ b/cpp/libclang/src/visitor/src/context.rs @@ -11,35 +11,82 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; -use class_diagram::{SimpleEntity, SourceLocation}; +use class_diagram::{FreeFunctionDecl, FunctionArgument, Method, SimpleEntity, SourceLocation}; use cpp_semantics::{FunctionDef, ResolvedType}; use serde::{Deserialize, Serialize}; -pub type TypeMap = HashMap; - -/// Identifies a function definition within one parser execution. +/// Identifies an AST entity within one parser execution. /// -/// This source-position key deduplicates project header definitions visible +/// This source-position key deduplicates project header declarations and definitions visible /// through multiple translation units. It is not stable across source revisions. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct FunctionDefinitionKey { +pub struct SourceEntityKey { pub source_file: PathBuf, pub source_offset: u32, } +/// Identifies a free function by its logical signature for class-diagram output. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CallableIdentityKey { + pub owner: CallableOwnerIdentityKey, + pub name: String, + pub parameters: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum CallableOwnerIdentityKey { + FreeFunction { + enclosing_namespace_id: Option, + }, + Method { + class_id: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CallableArgumentIdentityKey { + pub param_type: Option, + pub is_variadic: bool, + pub is_pack_expansion: bool, +} + +impl From<&FunctionArgument> for CallableArgumentIdentityKey { + fn from(argument: &FunctionArgument) -> Self { + Self { + param_type: argument.param_type.clone(), + is_variadic: argument.is_variadic, + is_pack_expansion: argument.is_pack_expansion, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExtractedFunction { - pub key: FunctionDefinitionKey, + pub key: SourceEntityKey, pub definition: FunctionDef, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExtractedFreeFunctionDeclaration { + pub key: SourceEntityKey, + pub declaration: FreeFunctionDecl, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExtractedMethodDeclaration { + pub class_id: String, + pub method: Method, + pub method_type: ParsedMethodType, +} + #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct VisitContext { - pub types: TypeMap, - pub parsed_class_info: Vec, + pub types: BTreeMap, + pub parsed_class_info: HashMap, + pub free_function_declarations: Vec, pub functions: Vec, } @@ -49,6 +96,8 @@ pub struct ParsedClassInfo { pub base_classes: Vec, pub variable_types: Vec, pub method_types: Vec, + pub has_abstract_methods: bool, + pub has_concrete_methods: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/cpp/libclang/src/visitor/src/context_ext.rs b/cpp/libclang/src/visitor/src/context_ext.rs new file mode 100644 index 00000000..0bb714a7 --- /dev/null +++ b/cpp/libclang/src/visitor/src/context_ext.rs @@ -0,0 +1,177 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +use log::warn; +use std::collections::BTreeMap; + +use class_diagram::SimpleEntity; + +pub trait EntityMapExt { + fn insert_or_merge_type(&mut self, type_name: String, entity: SimpleEntity); +} + +impl EntityMapExt for BTreeMap { + fn insert_or_merge_type(&mut self, type_name: String, entity: SimpleEntity) { + match self.get_mut(&type_name) { + Some(existing) => merge_simple_entity(existing, entity), + None => { + self.insert(type_name, entity); + } + } + } +} + +fn merge_simple_entity(existing: &mut SimpleEntity, incoming: SimpleEntity) { + if existing.name != incoming.name { + warn!( + "conflicting entity names while merging '{}': keeping {:?}, dropping {:?}", + existing.id, existing.name, incoming.name + ); + } + if existing.enclosing_namespace_id != incoming.enclosing_namespace_id { + warn!( + "conflicting enclosing namespaces while merging '{}': keeping {:?}, dropping {:?}", + existing.id, existing.enclosing_namespace_id, incoming.enclosing_namespace_id + ); + } + if existing.template_parameters.is_none() { + existing.template_parameters = incoming.template_parameters.clone(); + } + if existing.source_location == Default::default() { + existing.source_location = incoming.source_location.clone(); + } + + if existing.entity_type != incoming.entity_type { + warn!( + "conflicting entity types while merging '{}': keeping {:?}, dropping {:?}", + existing.id, existing.entity_type, incoming.entity_type + ); + } + + extend_unique(&mut existing.stereotypes, incoming.stereotypes); + extend_unique(&mut existing.type_aliases, incoming.type_aliases); + extend_unique(&mut existing.variables, incoming.variables); + extend_unique(&mut existing.methods, incoming.methods); + extend_unique(&mut existing.enum_literals, incoming.enum_literals); + extend_unique(&mut existing.relationships, incoming.relationships); +} + +fn extend_unique(existing: &mut Vec, incoming: Vec) { + for item in incoming { + if !existing.contains(&item) { + existing.push(item); + } + } +} + +#[cfg(test)] +mod tests { + use super::EntityMapExt; + use class_diagram::{EntityType, Method, SimpleEntity, SourceLocation, Visibility}; + use std::collections::BTreeMap; + + #[test] + fn insert_or_merge_type_preserves_members_when_later_entity_is_sparser() { + let mut types = BTreeMap::new(); + types.insert_or_merge_type( + "util::Widget".to_string(), + SimpleEntity { + id: "util::Widget".to_string(), + name: "Widget".to_string(), + enclosing_namespace_id: Some("util".to_string()), + entity_type: EntityType::Class, + methods: vec![Method { + name: "compute".to_string(), + return_type: Some("int".to_string()), + visibility: Visibility::Public, + source_location: SourceLocation::new("first.h", 10), + ..Default::default() + }], + source_location: SourceLocation::new("first.h", 1), + ..Default::default() + }, + ); + + types.insert_or_merge_type( + "util::Widget".to_string(), + SimpleEntity { + id: "util::Widget".to_string(), + name: "Widget".to_string(), + enclosing_namespace_id: Some("util".to_string()), + entity_type: EntityType::Class, + stereotypes: vec!["header-only".to_string()], + source_location: SourceLocation::new("second.h", 1), + ..Default::default() + }, + ); + + let widget = types.get("util::Widget").expect("merged type should exist"); + + assert_eq!(widget.methods.len(), 1); + assert_eq!(widget.methods[0].name, "compute"); + assert_eq!( + widget.methods[0].source_location, + SourceLocation::new("first.h", 10) + ); + assert_eq!(widget.stereotypes, vec!["header-only"]); + assert_eq!(widget.source_location, SourceLocation::new("first.h", 1)); + assert_eq!(widget.enclosing_namespace_id.as_deref(), Some("util")); + } + + #[test] + fn insert_or_merge_type_adds_missing_details_when_later_entity_is_richer() { + let mut types = BTreeMap::new(); + types.insert_or_merge_type( + "util::Widget".to_string(), + SimpleEntity { + id: "util::Widget".to_string(), + name: "Widget".to_string(), + enclosing_namespace_id: Some("util".to_string()), + entity_type: EntityType::Class, + ..Default::default() + }, + ); + + types.insert_or_merge_type( + "util::Widget".to_string(), + SimpleEntity { + id: "util::Widget".to_string(), + name: "Widget".to_string(), + enclosing_namespace_id: Some("util".to_string()), + entity_type: EntityType::Class, + methods: vec![Method { + name: "compute".to_string(), + return_type: Some("int".to_string()), + visibility: Visibility::Public, + source_location: SourceLocation::new("second.h", 10), + ..Default::default() + }], + stereotypes: vec!["header-only".to_string()], + source_location: SourceLocation::new("second.h", 1), + ..Default::default() + }, + ); + + let widget = types.get("util::Widget").expect("merged type should exist"); + + assert_eq!(widget.methods.len(), 1); + assert_eq!(widget.methods[0].name, "compute"); + assert_eq!( + widget.methods[0].source_location, + SourceLocation::new("second.h", 10) + ); + assert_eq!(widget.stereotypes, vec!["header-only"]); + assert_eq!(widget.source_location, SourceLocation::new("second.h", 1)); + assert_eq!(widget.enclosing_namespace_id.as_deref(), Some("util")); + } +} diff --git a/cpp/libclang/src/visitor/src/function_visitor.rs b/cpp/libclang/src/visitor/src/function_visitor.rs index 36cfc070..a96efcaf 100644 --- a/cpp/libclang/src/visitor/src/function_visitor.rs +++ b/cpp/libclang/src/visitor/src/function_visitor.rs @@ -15,16 +15,27 @@ //! Preserves structured calls, branches, and loops for supported AST shapes, //! and falls back to conservative traversal for unsupported control-flow forms. -use clang::{Entity, EntityKind}; +use clang::{Entity, EntityKind, ExceptionSpecification}; +use class_diagram::{FreeFunctionDecl, Method, MethodModifier}; use cpp_semantics::{ BodyItem, BranchCase, FunctionDef, FunctionId, FunctionKind, GuardExpression, LoopKind, + ResolvedType, Scope, }; use std::collections::HashSet; -use crate::clang_adapter::scope::callable_scope; +use crate::callable_declaration::{ + callable_arguments, parse_callable_return_type, parse_function_parameters, + parse_template_parameters, +}; +use crate::clang_adapter::scope::{callable_scope, namespace_id}; use crate::clang_adapter::source_filter; use crate::clang_adapter::source_location::parse_source_location; -use crate::context::{ExtractedFunction, FunctionDefinitionKey}; +use crate::class_visitor::{parse_visibility, ClassVisitor}; +use crate::context::{ + CallableArgumentIdentityKey, CallableIdentityKey, CallableOwnerIdentityKey, + ExtractedFreeFunctionDeclaration, ExtractedFunction, ExtractedMethodDeclaration, + ParsedMethodType, SourceEntityKey, +}; use crate::types::resolver::resolve_type; use crate::visitor::{normalize_source_identity_path, SourceFileCache}; use crate::VisitContext; @@ -43,24 +54,199 @@ impl FunctionVisitor { pub(crate) fn visit_with_state( ctx: &mut VisitContext, source_files: &mut SourceFileCache, - seen_function_definitions: &mut HashSet, + seen_free_function_declarations: &mut HashSet, + seen_method_declarations: &mut HashSet, + seen_function_definitions: &mut HashSet, entity: Entity, ) { - if let Some(function) = - Self::extract_function_def(source_files, seen_function_definitions, entity) - { + let Some((function_id, function_kind)) = Self::extract_callable(&entity) else { + return; + }; + + match &function_id.scope { + Scope::Type { .. } => { + if let Some(declaration) = Self::extract_method_declaration( + seen_method_declarations, + &entity, + &function_id, + function_kind, + ) { + ClassVisitor::add_method_declaration(ctx, declaration); + } else { + log::debug!( + "skipping type-scoped callable '{}': unsupported function kind {:?}", + function_id.qualified_name(), + function_kind + ); + } + } + Scope::Global | Scope::Namespace(_) => { + if let Some(declaration) = Self::extract_free_function_declaration( + seen_free_function_declarations, + &entity, + &function_id, + ) { + ctx.free_function_declarations.push(declaration); + } + } + } + + if let Some(function) = Self::extract_function_def( + entity, + function_id, + function_kind, + source_files, + seen_function_definitions, + ) { ctx.functions.push(function); } } // ── Top-level extraction ────────────────────────────────────────────────── + fn extract_method_declaration( + seen_method_declarations: &mut HashSet, + entity: &Entity, + id: &FunctionId, + kind: FunctionKind, + ) -> Option { + if !matches!( + kind, + FunctionKind::Method + | FunctionKind::StaticMethod + | FunctionKind::Constructor + | FunctionKind::Destructor + ) { + return None; + } + + let parameters = parse_function_parameters(entity); + let class_id = id.scope.qualified_name(); + if !Self::insert_callable_identity( + seen_method_declarations, + CallableOwnerIdentityKey::Method { + class_id: class_id.clone(), + }, + &id.name, + ¶meters, + ) { + return None; + } + + let return_type = entity + .get_result_type() + .map(|ty| resolve_type(&ty)) + .unwrap_or_else(|| ResolvedType::Builtin("void".to_string())); + let method_type = ParsedMethodType { + name: id.name.clone(), + return_type: return_type.clone(), + parameter_types: callable_arguments(entity) + .into_iter() + .filter_map(|argument| argument.get_type().map(|ty| resolve_type(&ty))) + .collect(), + source_location: parse_source_location(entity), + }; + + let is_override_method = entity + .get_overridden_methods() + .is_some_and(|methods| !methods.is_empty()); + + // Only the bare `noexcept` specifier is modeled (mirrors the PlantUML grammar, which has + // no support for the conditional `noexcept(expr)` form). Requiring `BasicNoexcept` filters + // out `noexcept(expr)`, but on its own it isn't enough: for an implicit/defaulted special + // member (e.g. `~Foo() = default;` with no written specifier at all), the compiler-computed + // specification also resolves to `BasicNoexcept` once evaluated -- and that evaluation is + // lazily triggered by unrelated code (e.g. a derived class use), making it unstable. So this + // also requires the literal `noexcept` token to appear in the declarator (the tokens up to + // the first `{` or `;`), which excludes both that case and `noexcept` written inside a + // lambda in the method body. + let has_noexcept_token = entity.get_range().is_some_and(|range| { + range + .tokenize() + .iter() + .take_while(|token| !matches!(token.get_spelling().as_str(), "{" | ";")) + .any(|token| token.get_spelling() == "noexcept") + }); + + let is_noexcept_method = has_noexcept_token + && matches!( + entity.get_exception_specification(), + Some(ExceptionSpecification::BasicNoexcept) + ); + + let return_type = if matches!(kind, FunctionKind::Constructor | FunctionKind::Destructor) { + None + } else { + parse_callable_return_type(entity) + }; + + let method = Method { + name: id.name.clone(), + return_type, + visibility: parse_visibility(entity), + parameters, + template_parameters: parse_template_parameters(entity), + modifiers: MethodModifier::from_conditions([ + (entity.is_static_method(), MethodModifier::Static), + (entity.is_virtual_method(), MethodModifier::Virtual), + (entity.is_pure_virtual_method(), MethodModifier::Abstract), + (is_override_method, MethodModifier::Override), + (is_noexcept_method, MethodModifier::Noexcept), + ( + kind == FunctionKind::Constructor, + MethodModifier::Constructor, + ), + (kind == FunctionKind::Destructor, MethodModifier::Destructor), + ]), + source_location: parse_source_location(entity), + }; + + Some(ExtractedMethodDeclaration { + class_id, + method, + method_type, + }) + } + + fn extract_free_function_declaration( + seen_free_function_declarations: &mut HashSet, + entity: &Entity, + id: &FunctionId, + ) -> Option { + let key = Self::extract_source_entity_key(entity)?; + let parameters = parse_function_parameters(entity); + if !Self::insert_callable_identity( + seen_free_function_declarations, + CallableOwnerIdentityKey::FreeFunction { + enclosing_namespace_id: namespace_id(entity), + }, + &id.name, + ¶meters, + ) { + return None; + } + + Some(ExtractedFreeFunctionDeclaration { + key, + declaration: FreeFunctionDecl { + name: id.name.clone(), + enclosing_namespace_id: namespace_id(entity), + return_type: parse_callable_return_type(entity), + parameters, + template_parameters: parse_template_parameters(entity), + source_location: parse_source_location(entity), + }, + }) + } + fn extract_function_def( - source_files: &mut SourceFileCache, - seen_function_definitions: &mut HashSet, entity: Entity, + id: FunctionId, + kind: FunctionKind, + source_files: &mut SourceFileCache, + seen_function_definitions: &mut HashSet, ) -> Option { - let key = Self::extract_definition_key(&entity)?; + let key = Self::extract_source_entity_key(&entity)?; if seen_function_definitions.contains(&key) { log::debug!( @@ -71,23 +257,6 @@ impl FunctionVisitor { return None; } - let Some(id) = Self::extract_function_id(&entity) else { - log::debug!( - "skipping callable '{}': no supported function identity", - entity.get_name().unwrap_or_default() - ); - return None; - }; - - let Some(kind) = Self::extract_function_kind(&entity) else { - log::debug!( - "skipping callable '{}': unsupported callable kind {:?}", - id.qualified_name(), - entity.get_kind() - ); - return None; - }; - let Some(body) = Self::process_function_body(source_files, entity, &id) else { log::debug!( "skipping callable '{}': no compound statement body (declaration-only?)", @@ -116,8 +285,30 @@ impl FunctionVisitor { Some(extracted_function) } + fn insert_callable_identity( + seen_declarations: &mut HashSet, + owner: CallableOwnerIdentityKey, + name: &str, + parameters: &[class_diagram::FunctionArgument], + ) -> bool { + seen_declarations.insert(CallableIdentityKey { + owner, + name: name.to_string(), + parameters: parameters + .iter() + .map(CallableArgumentIdentityKey::from) + .collect(), + }) + } + // ── AST navigation helpers ──────────────────────────────────────────────── + fn extract_callable(entity: &Entity) -> Option<(FunctionId, FunctionKind)> { + let function_id = Self::extract_function_id(entity)?; + let function_kind = Self::extract_function_kind(entity, &function_id.scope)?; + Some((function_id, function_kind)) + } + fn extract_function_id(entity: &Entity) -> Option { Some(FunctionId { scope: callable_scope(entity)?, @@ -125,9 +316,32 @@ impl FunctionVisitor { }) } - fn extract_definition_key(entity: &Entity) -> Option { + fn extract_function_kind(entity: &Entity, scope: &Scope) -> Option { + match entity.get_kind() { + EntityKind::FunctionDecl => Some(FunctionKind::Free), + EntityKind::FunctionTemplate => match scope { + Scope::Type { .. } => Some(Self::method_function_kind(entity)), + Scope::Global | Scope::Namespace(_) => Some(FunctionKind::Free), + }, + EntityKind::Method => Some(Self::method_function_kind(entity)), + EntityKind::Constructor => Some(FunctionKind::Constructor), + EntityKind::Destructor => Some(FunctionKind::Destructor), + EntityKind::ConversionFunction => Some(FunctionKind::Conversion), + _ => None, + } + } + + fn method_function_kind(entity: &Entity) -> FunctionKind { + if entity.is_static_method() { + FunctionKind::StaticMethod + } else { + FunctionKind::Method + } + } + + fn extract_source_entity_key(entity: &Entity) -> Option { let location = entity.get_location()?.get_file_location(); - Some(FunctionDefinitionKey { + Some(SourceEntityKey { source_file: normalize_source_identity_path(&location.file?.get_path()), source_offset: location.offset, }) @@ -164,31 +378,6 @@ impl FunctionVisitor { .unwrap_or_default() } - fn extract_function_kind(entity: &Entity) -> Option { - match entity.get_kind() { - EntityKind::FunctionDecl => Some(FunctionKind::Free), - EntityKind::FunctionTemplate => match callable_scope(entity)? { - cpp_semantics::Scope::Type { .. } => Some(if entity.is_static_method() { - FunctionKind::StaticMethod - } else { - FunctionKind::Method - }), - cpp_semantics::Scope::Global | cpp_semantics::Scope::Namespace(_) => { - Some(FunctionKind::Free) - } - }, - EntityKind::Method => Some(if entity.is_static_method() { - FunctionKind::StaticMethod - } else { - FunctionKind::Method - }), - EntityKind::Constructor => Some(FunctionKind::Constructor), - EntityKind::Destructor => Some(FunctionKind::Destructor), - EntityKind::ConversionFunction => Some(FunctionKind::Conversion), - _ => None, - } - } - /// Resolves a call expression to its semantic callable target. fn extract_call_target(call_expr: Entity) -> Option { // Direct reference works for simple `obj.method()` calls. @@ -205,8 +394,7 @@ impl FunctionVisitor { return None; } - Self::extract_function_kind(&resolved)?; - Self::extract_function_id(&resolved) + Self::extract_callable(&resolved).map(|(function_id, _)| function_id) } fn is_cross_owner_call(caller: &FunctionId, callee: &FunctionId) -> bool { diff --git a/cpp/libclang/src/visitor/src/lib.rs b/cpp/libclang/src/visitor/src/lib.rs index 9e87d26d..145a9bee 100644 --- a/cpp/libclang/src/visitor/src/lib.rs +++ b/cpp/libclang/src/visitor/src/lib.rs @@ -11,10 +11,12 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +mod callable_declaration; mod clang_adapter; mod class_relationship_resolver; mod class_visitor; pub mod context; +mod context_ext; mod enum_visitor; mod function_visitor; mod types; @@ -24,7 +26,8 @@ pub use cpp_semantics::{BodyItem, FunctionDef, ResolvedType}; pub use clang_adapter::source_filter::is_external_dependency_path; pub use class_visitor::ClassVisitor; -pub use context::{FunctionDefinitionKey, VisitContext}; +pub use context::{CallableIdentityKey, CallableOwnerIdentityKey, SourceEntityKey, VisitContext}; +pub use context_ext::EntityMapExt; pub use enum_visitor::EnumVisitor; pub use function_visitor::FunctionVisitor; pub use visitor::{AstVisitor, SourceFileCache, Visitor}; diff --git a/cpp/libclang/src/visitor/src/visitor.rs b/cpp/libclang/src/visitor/src/visitor.rs index cc49b8e5..0564fdba 100644 --- a/cpp/libclang/src/visitor/src/visitor.rs +++ b/cpp/libclang/src/visitor/src/visitor.rs @@ -19,7 +19,7 @@ use log::warn; use crate::clang_adapter::source_filter; use crate::class_visitor::ClassVisitor; -use crate::context::{FunctionDefinitionKey, VisitContext}; +use crate::context::{CallableIdentityKey, SourceEntityKey, VisitContext}; use crate::enum_visitor::EnumVisitor; use crate::function_visitor::FunctionVisitor; @@ -58,18 +58,24 @@ impl SourceFileCache { pub struct Visitor<'a> { ctx: &'a mut VisitContext, source_files: &'a mut SourceFileCache, - seen_function_definitions: &'a mut HashSet, + seen_free_function_declarations: &'a mut HashSet, + seen_method_declarations: &'a mut HashSet, + seen_function_definitions: &'a mut HashSet, } impl<'a> Visitor<'a> { pub fn new( ctx: &'a mut VisitContext, source_files: &'a mut SourceFileCache, - seen_function_definitions: &'a mut HashSet, + seen_free_function_declarations: &'a mut HashSet, + seen_method_declarations: &'a mut HashSet, + seen_function_definitions: &'a mut HashSet, ) -> Self { Self { ctx, source_files, + seen_free_function_declarations, + seen_method_declarations, seen_function_definitions, } } @@ -92,19 +98,22 @@ impl<'a> Visitor<'a> { ClassVisitor::visit(self.ctx, entity); } EntityKind::EnumDecl => EnumVisitor::visit(self.ctx, entity), - EntityKind::FunctionDecl | EntityKind::FunctionTemplate | EntityKind::Method => { + EntityKind::FunctionDecl + | EntityKind::FunctionTemplate + | EntityKind::Method + | EntityKind::Constructor + | EntityKind::Destructor => { FunctionVisitor::visit_with_state( self.ctx, self.source_files, + self.seen_free_function_declarations, + self.seen_method_declarations, self.seen_function_definitions, entity, ); } - EntityKind::Constructor | EntityKind::Destructor | EntityKind::ConversionFunction => { - warn!( - "Ignoring constructor, destructor, or conversion function: {:?}", - entity - ); + EntityKind::ConversionFunction => { + warn!("Ignoring conversion function: {:?}", entity); } _ => {} } diff --git a/plantuml/parser/puml_idmap/src/lib.rs b/plantuml/parser/puml_idmap/src/lib.rs index c8360081..46b1c293 100644 --- a/plantuml/parser/puml_idmap/src/lib.rs +++ b/plantuml/parser/puml_idmap/src/lib.rs @@ -627,6 +627,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![with_members, without_members], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/classes.puml"); @@ -677,6 +678,7 @@ mod tests { let model = ClassDiagram { name: "sorted".to_string(), entities: vec![with_members_z, ref_m, with_members_a, ref_b], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/class_sorted.puml"); @@ -782,6 +784,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![define], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/classes.puml"); @@ -815,6 +818,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![a, b], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/classes.puml"); @@ -851,6 +855,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![child], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/ns.puml"); @@ -897,6 +902,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![child], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/ns.puml"); @@ -928,6 +934,7 @@ mod tests { let model = ClassDiagram { name: "unit_1_class_diagram".to_string(), entities: vec![foo], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "unit_1/docs/unit_1_class_diagram.puml"); @@ -962,6 +969,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![child, container_as_real_entity], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/ns.puml"); @@ -995,6 +1003,7 @@ mod tests { let model = ClassDiagram { name: "Proxy".to_string(), entities: vec![proxy, leaf], + free_functions: vec![], }; let idmap = class_model_to_idmap(&model, "pkg/proxy.puml"); @@ -1129,6 +1138,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![with_members], + free_functions: vec![], }; let input = Path::new("some/dir/classes.puml"); diff --git a/plantuml/parser/puml_lobster/src/lib.rs b/plantuml/parser/puml_lobster/src/lib.rs index 050dfcaf..f5786511 100644 --- a/plantuml/parser/puml_lobster/src/lib.rs +++ b/plantuml/parser/puml_lobster/src/lib.rs @@ -261,6 +261,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![entity], + free_functions: vec![], }; let dir = unique_tmp_dir("class"); let input = Path::new("some/dir/classes.puml"); @@ -291,6 +292,7 @@ mod tests { let model = ClassDiagram { name: "d".to_string(), entities: vec![entity], + free_functions: vec![], }; let dir = unique_tmp_dir("class_override"); let input = Path::new("some/dir/classes.puml"); diff --git a/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs b/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs index 96996814..63ae3a26 100644 --- a/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs +++ b/plantuml/parser/puml_resolver/src/class_diagram/src/class_resolver.rs @@ -70,6 +70,7 @@ impl ClassResolver { logic: ClassDiagram { name: String::new(), entities: Vec::new(), + free_functions: Vec::new(), }, name_map: HashMap::new(), } @@ -844,6 +845,7 @@ impl DiagramResolver for ClassResolver { ClassDiagram { name: String::new(), entities: Vec::new(), + free_functions: Vec::new(), }, ); diff --git a/tools/metamodel/class/class_logic.rs b/tools/metamodel/class/class_logic.rs index 1a804d7e..a560ebf7 100644 --- a/tools/metamodel/class/class_logic.rs +++ b/tools/metamodel/class/class_logic.rs @@ -18,7 +18,10 @@ pub use source_location::SourceLocation; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct ClassDiagram { pub name: String, + #[serde(default)] pub entities: Vec, + #[serde(default)] + pub free_functions: Vec, } /// Represents a class, struct, interface, enum, or other type entity @@ -258,6 +261,33 @@ pub struct EnumLiteral { pub source_location: SourceLocation, } +/// Represents a global- or namespace-scope function declaration. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct FreeFunctionDecl { + /// Function name without its namespace qualification. + pub name: String, + /// Namespace containing the function, if any. + pub enclosing_namespace_id: Option, + /// Return type. + pub return_type: Option, + /// Function parameters. + pub parameters: Vec, + /// Template parameters for generic functions. + pub template_parameters: Option>, + /// Source location in input. + pub source_location: SourceLocation, +} + +impl FreeFunctionDecl { + /// Returns the function name qualified by its containing namespace. + pub fn qualified_name(&self) -> String { + match self.enclosing_namespace_id.as_deref() { + Some(namespace) if !namespace.is_empty() => format!("{namespace}::{}", self.name), + _ => self.name.clone(), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -312,6 +342,22 @@ mod tests { assert_eq!(inheritance.relation_type, RelationType::Inheritance); } + #[test] + fn free_function_qualified_name_includes_namespace_when_present() { + let global = FreeFunctionDecl { + name: "log".to_string(), + ..Default::default() + }; + let namespaced = FreeFunctionDecl { + name: "log".to_string(), + enclosing_namespace_id: Some("app::internal".to_string()), + ..Default::default() + }; + + assert_eq!(global.qualified_name(), "log"); + assert_eq!(namespaced.qualified_name(), "app::internal::log"); + } + #[test] fn test_partial_plantuml_entity() { // PlantUML often has incomplete information - this should still work diff --git a/validation/core/src/models/class_diagram_models.rs b/validation/core/src/models/class_diagram_models.rs index d0d68ad9..658b2105 100644 --- a/validation/core/src/models/class_diagram_models.rs +++ b/validation/core/src/models/class_diagram_models.rs @@ -206,6 +206,7 @@ mod tests { entity("Unit.Sample", "design_a.puml", 12), entity("unit.sample", "design_b.puml", 34), ], + free_functions: Vec::new(), }]; let mut result = ValidationResult::default(); @@ -251,6 +252,7 @@ mod tests { source_location: SourceLocation::new("test.puml", 1), }, ], + free_functions: Vec::new(), }]; let index = InternalApiIndex::build_index(&diagrams); @@ -300,6 +302,7 @@ mod tests { source_location: SourceLocation::new("test.puml", 1), }, ], + free_functions: Vec::new(), }]; let index = InternalApiIndex::build_index(&diagrams); diff --git a/validation/core/src/readers/class_diagram_reader.rs b/validation/core/src/readers/class_diagram_reader.rs index 171e23fe..e93b5293 100644 --- a/validation/core/src/readers/class_diagram_reader.rs +++ b/validation/core/src/readers/class_diagram_reader.rs @@ -324,6 +324,7 @@ impl Reader for ClassDiagramReader { diagrams.push(ClassDiagram { name: diagram.name().to_string(), entities, + free_functions: Vec::new(), }); } diff --git a/validation/core/src/validators/class_design_implementation_validator.rs b/validation/core/src/validators/class_design_implementation_validator.rs index e7a4159d..b43b27fd 100644 --- a/validation/core/src/validators/class_design_implementation_validator.rs +++ b/validation/core/src/validators/class_design_implementation_validator.rs @@ -1102,6 +1102,7 @@ mod tests { let diagrams: ClassDiagramInputs = vec![ClassDiagram { name: "unit".to_string(), entities, + free_functions: Vec::new(), }]; ClassEntityIndex::build_index(&diagrams, &mut ValidationResult::default()) } diff --git a/validation/core/src/validators/test/class_design_sequence_validator_test.rs b/validation/core/src/validators/test/class_design_sequence_validator_test.rs index a85cbfca..9bb52a2f 100644 --- a/validation/core/src/validators/test/class_design_sequence_validator_test.rs +++ b/validation/core/src/validators/test/class_design_sequence_validator_test.rs @@ -37,6 +37,7 @@ fn class_diagrams(entities: Vec) -> ClassDiagramInp vec![ClassDiagram { name: "class_design".to_string(), entities, + free_functions: Vec::new(), }] } diff --git a/validation/core/src/validators/test/component_public_api_validator_test.rs b/validation/core/src/validators/test/component_public_api_validator_test.rs index dfdfaf89..8c7a9504 100644 --- a/validation/core/src/validators/test/component_public_api_validator_test.rs +++ b/validation/core/src/validators/test/component_public_api_validator_test.rs @@ -41,6 +41,7 @@ fn public_api_index(interfaces: Vec<(&str, Option<&str>)>) -> PublicApiIndex { .into_iter() .map(|(interface_name, namespace)| class_interface(interface_name, namespace)) .collect(), + free_functions: Vec::new(), }]; PublicApiIndex::build_index(&diagrams) diff --git a/validation/core/src/validators/test/fixtures.rs b/validation/core/src/validators/test/fixtures.rs index 15f35966..a146d7f0 100644 --- a/validation/core/src/validators/test/fixtures.rs +++ b/validation/core/src/validators/test/fixtures.rs @@ -187,6 +187,7 @@ pub(super) fn internal_api_index(interfaces: Vec<(&str, Vec<&str>)>) -> Internal interface }) .collect(), + free_functions: Vec::new(), }]; InternalApiIndex::build_index(&diagrams)