From 8cbaec562f1d56734aea27613eb22d9cefc84b28 Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Mon, 17 Aug 2026 18:08:25 +0800 Subject: [PATCH 1/2] refactor(libclang): separate visitor extraction concerns --- cpp/libclang/src/semantics/BUILD | 30 + cpp/libclang/src/semantics/src/lib.rs | 16 + .../src/semantics/src/resolved_type.rs | 362 +++++++++ cpp/libclang/src/visitor/BUILD | 32 +- .../src/visitor/src/clang_adapter/mod.rs | 18 + .../src/visitor/src/clang_adapter/scope.rs | 44 ++ .../src/{ => clang_adapter}/source_filter.rs | 14 +- .../src/clang_adapter/source_location.rs | 29 + .../src/visitor/src/class_parser_helper.rs | 701 ------------------ .../visitor/src/class_parser_helper_test.rs | 251 ------- .../src/class_relationship_resolver.rs | 445 +++++++++++ cpp/libclang/src/visitor/src/class_visitor.rs | 260 +------ .../src/visitor/src/class_visitor_test.rs | 225 ------ cpp/libclang/src/visitor/src/context.rs | 6 +- cpp/libclang/src/visitor/src/enum_visitor.rs | 10 +- cpp/libclang/src/visitor/src/lib.rs | 12 +- cpp/libclang/src/visitor/src/types/mod.rs | 17 + .../src/visitor/src/types/renderer.rs | 70 ++ .../src/visitor/src/types/resolver.rs | 339 +++++++++ cpp/libclang/src/visitor/src/visitor.rs | 31 +- 20 files changed, 1435 insertions(+), 1477 deletions(-) create mode 100644 cpp/libclang/src/semantics/BUILD create mode 100644 cpp/libclang/src/semantics/src/lib.rs create mode 100644 cpp/libclang/src/semantics/src/resolved_type.rs create mode 100644 cpp/libclang/src/visitor/src/clang_adapter/mod.rs create mode 100644 cpp/libclang/src/visitor/src/clang_adapter/scope.rs rename cpp/libclang/src/visitor/src/{ => clang_adapter}/source_filter.rs (90%) create mode 100644 cpp/libclang/src/visitor/src/clang_adapter/source_location.rs delete mode 100644 cpp/libclang/src/visitor/src/class_parser_helper.rs delete mode 100644 cpp/libclang/src/visitor/src/class_parser_helper_test.rs create mode 100644 cpp/libclang/src/visitor/src/class_relationship_resolver.rs delete mode 100644 cpp/libclang/src/visitor/src/class_visitor_test.rs create mode 100644 cpp/libclang/src/visitor/src/types/mod.rs create mode 100644 cpp/libclang/src/visitor/src/types/renderer.rs create mode 100644 cpp/libclang/src/visitor/src/types/resolver.rs diff --git a/cpp/libclang/src/semantics/BUILD b/cpp/libclang/src/semantics/BUILD new file mode 100644 index 00000000..912f4609 --- /dev/null +++ b/cpp/libclang/src/semantics/BUILD @@ -0,0 +1,30 @@ +# ******************************************************************************* +# 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("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "cpp_semantics", + srcs = [ + "src/lib.rs", + "src/resolved_type.rs", + ], + visibility = ["//cpp/libclang:__subpackages__"], + deps = ["@crates//:serde"], +) + +rust_test( + name = "resolved_type_test", + srcs = ["src/resolved_type.rs"], + deps = ["@crates//:serde"], +) diff --git a/cpp/libclang/src/semantics/src/lib.rs b/cpp/libclang/src/semantics/src/lib.rs new file mode 100644 index 00000000..51418dde --- /dev/null +++ b/cpp/libclang/src/semantics/src/lib.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +mod resolved_type; + +pub use resolved_type::{EntityId, ResolvedType}; diff --git a/cpp/libclang/src/semantics/src/resolved_type.rs b/cpp/libclang/src/semantics/src/resolved_type.rs new file mode 100644 index 00000000..a8d055ef --- /dev/null +++ b/cpp/libclang/src/semantics/src/resolved_type.rs @@ -0,0 +1,362 @@ +// ******************************************************************************* +// 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 serde::{Deserialize, Serialize}; + +pub type EntityId = String; + +/// Language-level representation of a C++ type after libclang extraction. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ResolvedType { + Builtin(String), + UserDefined(EntityId), + Template { + base: EntityId, + args: Vec, + }, + Function { + return_type: Box, + parameter_types: Vec, + is_variadic: bool, + }, + FunctionPointer(Box), + FunctionReference(Box), + Pointer(Box), + Reference(Box), + RValueReference(Box), + Const(Box), + Volatile(Box), + Array { + element: Box, + size: Option, + }, + Unknown(String), + /// A type that is structurally unresolvable without template instantiation, + /// e.g. `decltype(some_trait_impl(std::declval()))` inside an + /// uninstantiated template (the common SFINAE/trait-detection idiom). This is + /// distinct from `Unknown`: it is an expected, permanent limitation of + /// AST-only analysis rather than a gap in the resolver, so callers should not + /// treat it as an error condition. + Dependent(String), +} + +impl ResolvedType { + /// Returns whether this type should be treated as non-owning for relationship inference. + /// + /// Notes: + /// - Pointer/reference/function-pointer wrappers are always non-owning. + /// - Qualifiers and containers recurse into the wrapped/contained type. + /// - A few standard wrappers are modeled as non-owning by policy. + pub fn is_non_owning(&self) -> bool { + match self { + Self::Pointer(_) + | Self::Reference(_) + | Self::RValueReference(_) + | Self::FunctionPointer(_) + | Self::FunctionReference(_) => true, + Self::Const(inner) | Self::Volatile(inner) => inner.is_non_owning(), + Self::Function { + return_type, + parameter_types, + .. + } => { + return_type.is_non_owning() + || parameter_types.iter().any(ResolvedType::is_non_owning) + } + Self::Array { element, .. } => element.is_non_owning(), + Self::Template { base, args } => { + matches!( + base.trim_start_matches("::"), + "std::weak_ptr" + | "std::shared_ptr" + | "std::reference_wrapper" + | "std::observer_ptr" + ) || args.iter().any(ResolvedType::is_non_owning) + } + _ => false, + } + } + + /// Extracts a candidate relationship target entity id from a resolved type tree. + /// + /// Traversal policy: + /// - Template prefers first resolvable argument, then falls back to template base. + /// This intentionally keeps relationship targets at the template-family + /// level for now, even when the model also contains partial-specialization + /// entities with more specific ids. + /// - Function prefers return type, then parameter types. + /// - Wrapper/qualifier/array nodes delegate to their inner element. + pub fn relationship_target_entity_id(&self) -> Option<&str> { + match self { + Self::Builtin(_) | Self::Unknown(_) | Self::Dependent(_) => None, + Self::UserDefined(id) => Some(id), + Self::Template { base, args } => args + .iter() + .find_map(ResolvedType::relationship_target_entity_id) + .or(Some(base)), + Self::Function { + return_type, + parameter_types, + .. + } => return_type.relationship_target_entity_id().or_else(|| { + parameter_types + .iter() + .find_map(ResolvedType::relationship_target_entity_id) + }), + Self::FunctionPointer(inner) + | Self::FunctionReference(inner) + | Self::Pointer(inner) + | Self::Reference(inner) + | Self::RValueReference(inner) + | Self::Const(inner) + | Self::Volatile(inner) => inner.relationship_target_entity_id(), + Self::Array { element, .. } => element.relationship_target_entity_id(), + } + } + + /// Returns a direct referenced entity id for base-type style lookups. + /// + /// Unlike `relationship_target_entity_id`, this intentionally keeps template-base + /// semantics for inheritance resolution and does not attempt to target a + /// particular partial specialization entity. + pub fn referenced_entity_id(&self) -> Option<&str> { + match self.referenced_entity_root() { + Self::UserDefined(id) => Some(id), + Self::Template { base, .. } => Some(base), + _ => None, + } + } + + /// Unwraps qualifiers/wrappers to the core entity-bearing node. + /// + /// This helper is used by `referenced_entity_id` so that ownership/indirection + /// wrappers do not affect base-type lookup. + fn referenced_entity_root(&self) -> &ResolvedType { + match self { + Self::FunctionPointer(inner) + | Self::FunctionReference(inner) + | Self::Pointer(inner) + | Self::Reference(inner) + | Self::RValueReference(inner) + | Self::Const(inner) + | Self::Volatile(inner) => inner.referenced_entity_root(), + Self::Array { element, .. } => element.referenced_entity_root(), + _ => self, + } + } + + pub fn render_for_display(&self) -> String { + normalize_pointer_reference_spacing(self.render()) + } + + fn render(&self) -> String { + match self { + Self::Builtin(name) + | Self::UserDefined(name) + | Self::Unknown(name) + | Self::Dependent(name) => name.clone(), + Self::Template { base, args } => format!( + "{base}<{}>", + args.iter().map(Self::render).collect::>().join(", ") + ), + Self::Function { + return_type, + parameter_types, + is_variadic, + } => { + let mut parameters = parameter_types.iter().map(Self::render).collect::>(); + if *is_variadic { + parameters.push("...".to_string()); + } + format!("{}({})", return_type.render(), parameters.join(", ")) + } + Self::FunctionPointer(inner) => render_function_wrapper(inner, "*"), + Self::FunctionReference(inner) => render_function_wrapper(inner, "&"), + Self::Pointer(inner) => format!("{}*", inner.render()), + Self::Reference(inner) => format!("{}&", inner.render()), + Self::RValueReference(inner) => format!("{}&&", inner.render()), + Self::Const(inner) => match inner.as_ref() { + Self::Pointer(pointee) => format!("{}*const", pointee.render()), + _ => format!("const {}", inner.render()), + }, + Self::Volatile(inner) => format!("volatile {}", inner.render()), + Self::Array { element, size } => match size { + Some(size) => format!("{}[{size}]", element.render()), + None => format!("{}[]", element.render()), + }, + } + } +} + +fn render_function_wrapper(inner: &ResolvedType, marker: &str) -> String { + if let ResolvedType::Function { + return_type, + parameter_types, + is_variadic, + } = inner + { + let mut parameters = parameter_types + .iter() + .map(ResolvedType::render) + .collect::>(); + if *is_variadic { + parameters.push("...".to_string()); + } + format!( + "{} ({marker})({})", + return_type.render(), + parameters.join(", ") + ) + } else { + format!("{}{marker}", inner.render()) + } +} + +/// Normalizes spacing before pointer and reference markers for display. +/// +/// Rules: +/// - Insert one space before the first marker in a consecutive `*` or `&` sequence. +/// - Keep subsequent markers adjacent, preserving `**` and `&&`. +/// - Preserve function pointer/reference markers in `(*)` and `(&)`, whose +/// parentheses already provide separation. +/// +/// Examples: +// `Type*` -> `Type *` +// `Type**` -> `Type **` +/// `Type&&` -> `Type &&` +/// `void(*)(T)` -> `void (*)(T)` +fn normalize_pointer_reference_spacing(type_name: String) -> String { + let chars: Vec = type_name.chars().collect(); + let mut output = String::with_capacity(type_name.len() + 8); + + for (index, character) in chars.iter().copied().enumerate() { + if is_pointer_or_reference(character) && !is_function_pointer_marker(&chars, index) { + insert_space_before_pointer_marker(&mut output, &chars, index); + } + output.push(character); + } + + output +} + +fn is_pointer_or_reference(character: char) -> bool { + matches!(character, '*' | '&') +} + +fn is_function_pointer_marker(characters: &[char], index: usize) -> bool { + matches!(characters.get(index), Some('*' | '&')) + && matches!( + ( + index.checked_sub(1).and_then(|i| characters.get(i)), + characters.get(index + 1) + ), + (Some('('), Some(')')) + ) +} + +/// Adds a separator before the first marker unless one is already present. +fn insert_space_before_pointer_marker(output: &mut String, characters: &[char], index: usize) { + let previous_input = index + .checked_sub(1) + .and_then(|i| characters.get(i)) + .copied(); + + let is_first_marker = !matches!(previous_input, Some('*' | '&')); + if is_first_marker && !output.ends_with(' ') && !output.ends_with('(') { + output.push(' '); + } +} + +#[cfg(test)] +mod tests { + use super::ResolvedType; + + #[test] + fn resolves_referenced_entities_through_wrappers() { + let wrapped = ResolvedType::Const(Box::new(ResolvedType::Pointer(Box::new( + ResolvedType::UserDefined("Vehicle::Engine".to_string()), + )))); + assert_eq!(wrapped.referenced_entity_id(), Some("Vehicle::Engine")); + + let function_pointer = + ResolvedType::FunctionPointer(Box::new(ResolvedType::Const(Box::new( + ResolvedType::Pointer(Box::new(ResolvedType::UserDefined("Engine".to_string()))), + )))); + assert_eq!(function_pointer.referenced_entity_id(), Some("Engine")); + + let template = ResolvedType::Reference(Box::new(ResolvedType::Template { + base: "std::vector".to_string(), + args: vec![ResolvedType::UserDefined("Vehicle::Engine".to_string())], + })); + assert_eq!(template.referenced_entity_id(), Some("std::vector")); + } + + #[test] + fn relationship_target_prefers_template_argument() { + let ty = ResolvedType::Template { + base: "std::vector".to_string(), + args: vec![ResolvedType::UserDefined("Vehicle::Engine".to_string())], + }; + assert_eq!(ty.relationship_target_entity_id(), Some("Vehicle::Engine")); + } + + #[test] + fn renders_composite_types() { + let pointer = ResolvedType::Pointer(Box::new(ResolvedType::UserDefined( + "MyNamespace::Engine".to_string(), + ))); + assert_eq!(pointer.render_for_display(), "MyNamespace::Engine *"); + + let array = ResolvedType::Array { + element: Box::new(ResolvedType::Builtin("int".to_string())), + size: Some(8), + }; + assert_eq!(array.render_for_display(), "int[8]"); + } + + #[test] + fn renders_qualified_and_callable_types() { + let const_pointer = ResolvedType::Const(Box::new(ResolvedType::Pointer(Box::new( + ResolvedType::Builtin("int".to_string()), + )))); + assert_eq!(const_pointer.render_for_display(), "int *const"); + + let function = ResolvedType::Function { + return_type: Box::new(ResolvedType::Builtin("void".to_string())), + parameter_types: vec![ResolvedType::UserDefined("Engine".to_string())], + is_variadic: false, + }; + assert_eq!( + ResolvedType::FunctionPointer(Box::new(function)).render_for_display(), + "void (*)(Engine)" + ); + } + + #[test] + fn identifies_non_owning_and_dependent_types() { + let shared_ptr = ResolvedType::Template { + base: "std::shared_ptr".to_string(), + args: vec![ResolvedType::UserDefined("Engine".to_string())], + }; + assert!(shared_ptr.is_non_owning()); + + let dependent = ResolvedType::Dependent("decltype(foo(std::declval()))".to_string()); + assert_eq!(dependent.referenced_entity_id(), None); + assert_eq!(dependent.relationship_target_entity_id(), None); + assert!(!dependent.is_non_owning()); + assert_eq!( + dependent.render_for_display(), + "decltype(foo(std::declval()))" + ); + } +} diff --git a/cpp/libclang/src/visitor/BUILD b/cpp/libclang/src/visitor/BUILD index 452498cf..64f96fa0 100644 --- a/cpp/libclang/src/visitor/BUILD +++ b/cpp/libclang/src/visitor/BUILD @@ -15,17 +15,24 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") rust_library( name = "visit_tu", srcs = [ - "src/class_parser_helper.rs", + "src/clang_adapter/mod.rs", + "src/clang_adapter/scope.rs", + "src/clang_adapter/source_filter.rs", + "src/clang_adapter/source_location.rs", + "src/class_relationship_resolver.rs", "src/class_visitor.rs", "src/context.rs", "src/enum_visitor.rs", "src/function_visitor.rs", "src/lib.rs", - "src/source_filter.rs", + "src/types/mod.rs", + "src/types/renderer.rs", + "src/types/resolver.rs", "src/visitor.rs", ], visibility = ["//cpp/libclang:__subpackages__"], deps = [ + "//cpp/libclang/src/semantics:cpp_semantics", "//tools/metamodel/class:class_diagram", "//tools/metamodel/sequence:sequence_diagram", "@crates//:clang", @@ -35,24 +42,15 @@ rust_library( ) rust_test( - name = "class_parser_helper_test", - srcs = [ - "src/class_parser_helper.rs", - "src/class_parser_helper_test.rs", - "src/source_filter.rs", - ], + name = "visitor_unit_tests", + srcs = glob(["src/**/*.rs"]), + crate_root = "src/lib.rs", deps = [ + "//cpp/libclang/src/semantics:cpp_semantics", + "//tools/metamodel/class:class_diagram", + "//tools/metamodel/sequence:sequence_diagram", "@crates//:clang", "@crates//:log", "@crates//:serde", ], ) - -rust_test( - name = "class_visitor_source_location_test", - srcs = ["src/class_visitor_test.rs"], - deps = [ - ":visit_tu", - "//tools/metamodel/class:class_diagram", - ], -) diff --git a/cpp/libclang/src/visitor/src/clang_adapter/mod.rs b/cpp/libclang/src/visitor/src/clang_adapter/mod.rs new file mode 100644 index 00000000..aef877d8 --- /dev/null +++ b/cpp/libclang/src/visitor/src/clang_adapter/mod.rs @@ -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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* + +//! Adapters from libclang entities and types to visitor-local concepts. + +pub(crate) mod scope; +pub(crate) mod source_filter; +pub(crate) mod source_location; diff --git a/cpp/libclang/src/visitor/src/clang_adapter/scope.rs b/cpp/libclang/src/visitor/src/clang_adapter/scope.rs new file mode 100644 index 00000000..1b0ccb0e --- /dev/null +++ b/cpp/libclang/src/visitor/src/clang_adapter/scope.rs @@ -0,0 +1,44 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Shared semantic-scope extraction helpers for libclang entities. + +use clang::{Entity, EntityKind}; + +/// Returns the enclosing namespace names from outermost to innermost. +pub(crate) fn namespace_path(entity: &Entity) -> Vec { + let mut namespaces = Vec::new(); + let mut current = entity.get_semantic_parent(); + + while let Some(parent) = current { + if parent.get_kind() == EntityKind::Namespace { + // Anonymous namespaces have no stable name in libclang and are intentionally + // treated as transparent scopes. The current model does not distinguish + // same-named declarations from separate anonymous namespaces. + if let Some(name) = parent.get_name() { + namespaces.push(name); + } + } + current = parent.get_semantic_parent(); + } + + namespaces.reverse(); + namespaces +} + +/// Returns the enclosing namespace as a C++ qualified identifier. +pub(crate) fn namespace_id(entity: &Entity) -> Option { + let path = namespace_path(entity); + (!path.is_empty()).then(|| path.join("::")) +} + diff --git a/cpp/libclang/src/visitor/src/source_filter.rs b/cpp/libclang/src/visitor/src/clang_adapter/source_filter.rs similarity index 90% rename from cpp/libclang/src/visitor/src/source_filter.rs rename to cpp/libclang/src/visitor/src/clang_adapter/source_filter.rs index 6738f561..e1f4c220 100644 --- a/cpp/libclang/src/visitor/src/source_filter.rs +++ b/cpp/libclang/src/visitor/src/clang_adapter/source_filter.rs @@ -11,6 +11,8 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use clang::Type; + const SYSTEM_HEADER_PREFIXES: &[&str] = &["/usr/include", "/usr/local/include", "/opt/"]; const SYSTEM_HEADER_SUBSTRINGS: &[&str] = &["/gcc/"]; const EXTERNAL_DEP_PATH_SUBSTRINGS: &[&str] = &["/external/", "external/", "_virtual_includes/"]; @@ -39,6 +41,17 @@ pub(crate) fn is_external_or_system_path(path: &str) -> bool { is_system_header_path(path) || is_external_dependency_path(path) } +/// Returns whether a type's declaration belongs to an external or system header. +pub(crate) fn is_declared_in_external_or_system_header(ty: &Type) -> bool { + ty.get_declaration() + .and_then(|declaration| declaration.get_location()) + .map(|location| { + let (path, ..) = location.get_presumed_location(); + is_external_or_system_path(&path) + }) + .unwrap_or(false) +} + /// Returns whether an entity namespace should be excluded from the parsed model /// even when the source file is part of the workspace. pub(crate) fn is_excluded_namespace(namespace: Option<&str>) -> bool { @@ -88,7 +101,6 @@ mod tests { #[test] fn keeps_workspace_sources_in_the_model() { let path = "cpp/application/include/application/car.h"; - assert!(!is_system_header_path(path)); assert!(!is_external_dependency_path(path)); assert!(!is_external_or_system_path(path)); diff --git a/cpp/libclang/src/visitor/src/clang_adapter/source_location.rs b/cpp/libclang/src/visitor/src/clang_adapter/source_location.rs new file mode 100644 index 00000000..9d19cdcf --- /dev/null +++ b/cpp/libclang/src/visitor/src/clang_adapter/source_location.rs @@ -0,0 +1,29 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Conversion helpers between libclang locations and the shared metamodel. + +use clang::Entity; +use class_diagram::SourceLocation; + +pub(crate) fn parse_source_location(entity: &Entity) -> SourceLocation { + let Some(location) = entity.get_location() else { + return SourceLocation::default(); + }; + + let file_location = location.get_file_location(); + let source_file = file_location + .file + .map(|file| file.get_path().to_string_lossy().to_string()); + SourceLocation::new(source_file.unwrap_or_default(), file_location.line) +} diff --git a/cpp/libclang/src/visitor/src/class_parser_helper.rs b/cpp/libclang/src/visitor/src/class_parser_helper.rs deleted file mode 100644 index 01324105..00000000 --- a/cpp/libclang/src/visitor/src/class_parser_helper.rs +++ /dev/null @@ -1,701 +0,0 @@ -// ******************************************************************************* -// 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 -// ******************************************************************************* - -#![cfg_attr(test, allow(dead_code))] - -use crate::source_filter; -use clang::{Entity, EntityKind, Type, TypeKind}; -use serde::{Deserialize, Serialize}; - -pub type EntityId = String; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum ResolvedType { - Builtin(String), - - UserDefined(EntityId), - - Template { - base: EntityId, - args: Vec, - }, - - Function { - return_type: Box, - parameter_types: Vec, - is_variadic: bool, - }, - FunctionPointer(Box), - FunctionReference(Box), - - Pointer(Box), - Reference(Box), - RValueReference(Box), - Const(Box), - Volatile(Box), - - Array { - element: Box, - size: Option, - }, - - Unknown(String), - - /// A type that is structurally unresolvable without template instantiation, - /// e.g. `decltype(some_trait_impl(std::declval()))` inside an - /// uninstantiated template (the common SFINAE/trait-detection idiom). This is - /// distinct from `Unknown`: it is an expected, permanent limitation of - /// AST-only analysis rather than a gap in the resolver, so callers should not - /// treat it as an error condition. - Dependent(String), -} - -impl ResolvedType { - /// Returns whether this type should be treated as non-owning for relationship inference. - /// - /// Notes: - /// - Pointer/reference/function-pointer wrappers are always non-owning. - /// - Qualifiers and containers recurse into the wrapped/contained type. - /// - A few standard wrappers are modeled as non-owning by policy. - pub fn is_non_owning(&self) -> bool { - match self { - ResolvedType::Pointer(_) - | ResolvedType::Reference(_) - | ResolvedType::RValueReference(_) - | ResolvedType::FunctionPointer(_) - | ResolvedType::FunctionReference(_) => true, - ResolvedType::Const(inner) | ResolvedType::Volatile(inner) => inner.is_non_owning(), - ResolvedType::Function { - return_type, - parameter_types, - .. - } => { - return_type.is_non_owning() - || parameter_types.iter().any(ResolvedType::is_non_owning) - } - ResolvedType::Array { element, .. } => element.is_non_owning(), - ResolvedType::Template { base, args } => { - let normalized_base = base.trim_start_matches("::"); - matches!( - normalized_base, - "std::weak_ptr" - | "std::shared_ptr" - | "std::reference_wrapper" - | "std::observer_ptr" - ) || args.iter().any(ResolvedType::is_non_owning) - } - _ => false, - } - } - - /// Extracts a candidate relationship target entity id from a resolved type tree. - /// - /// Traversal policy: - /// - Template prefers first resolvable argument, then falls back to template base. - /// This intentionally keeps relationship targets at the template-family - /// level for now, even when the model also contains partial-specialization - /// entities with more specific ids. - /// - Function prefers return type, then parameter types. - /// - Wrapper/qualifier/array nodes delegate to their inner element. - pub fn relationship_target_entity_id(&self) -> Option<&str> { - match self { - ResolvedType::Builtin(_) | ResolvedType::Unknown(_) | ResolvedType::Dependent(_) => { - None - } - ResolvedType::UserDefined(id) => Some(id), - ResolvedType::Template { base, args } => args - .iter() - .find_map(ResolvedType::relationship_target_entity_id) - .or_else(|| Some(base)), - ResolvedType::Function { - return_type, - parameter_types, - .. - } => return_type.relationship_target_entity_id().or_else(|| { - parameter_types - .iter() - .find_map(ResolvedType::relationship_target_entity_id) - }), - ResolvedType::FunctionPointer(inner) - | ResolvedType::FunctionReference(inner) - | ResolvedType::Pointer(inner) - | ResolvedType::Reference(inner) - | ResolvedType::RValueReference(inner) - | ResolvedType::Const(inner) - | ResolvedType::Volatile(inner) => inner.relationship_target_entity_id(), - ResolvedType::Array { element, .. } => element.relationship_target_entity_id(), - } - } - - /// Returns a direct referenced entity id for base-type style lookups. - /// - /// Unlike `relationship_target_entity_id`, this intentionally keeps template-base - /// semantics for inheritance resolution and does not attempt to target a - /// particular partial specialization entity. - pub fn referenced_entity_id(&self) -> Option<&str> { - match self.referenced_entity_root() { - ResolvedType::UserDefined(id) => Some(id), - ResolvedType::Template { base, .. } => Some(base), - _ => None, - } - } - - /// Unwraps qualifiers/wrappers to the core entity-bearing node. - /// - /// This helper is used by `referenced_entity_id` so that ownership/indirection - /// wrappers do not affect base-type lookup. - fn referenced_entity_root(&self) -> &ResolvedType { - match self { - ResolvedType::FunctionPointer(inner) - | ResolvedType::FunctionReference(inner) - | ResolvedType::Pointer(inner) - | ResolvedType::Reference(inner) - | ResolvedType::RValueReference(inner) - | ResolvedType::Const(inner) - | ResolvedType::Volatile(inner) => inner.referenced_entity_root(), - - ResolvedType::Array { element, .. } => element.referenced_entity_root(), - - _ => self, - } - } - - fn render(&self) -> String { - match self { - ResolvedType::Builtin(name) => name.clone(), - - ResolvedType::UserDefined(id) => id.clone(), - - ResolvedType::Template { base, args } => { - let args = args - .iter() - .map(|arg| arg.render()) - .collect::>() - .join(", "); - - format!("{base}<{args}>") - } - - ResolvedType::Function { - return_type, - parameter_types, - is_variadic, - } => { - let mut params = parameter_types - .iter() - .map(|p| p.render()) - .collect::>(); - if *is_variadic { - params.push("...".to_string()); - } - format!("{}({})", return_type.render(), params.join(", ")) - } - - ResolvedType::FunctionPointer(inner) => { - if let ResolvedType::Function { - return_type, - parameter_types, - is_variadic, - } = inner.as_ref() - { - let mut params = parameter_types - .iter() - .map(|p| p.render()) - .collect::>(); - if *is_variadic { - params.push("...".to_string()); - } - format!("{} (*)({})", return_type.render(), params.join(", ")) - } else { - format!("{}*", inner.render()) - } - } - - ResolvedType::FunctionReference(inner) => { - if let ResolvedType::Function { - return_type, - parameter_types, - is_variadic, - } = inner.as_ref() - { - let mut params = parameter_types - .iter() - .map(|p| p.render()) - .collect::>(); - if *is_variadic { - params.push("...".to_string()); - } - format!("{} (&)({})", return_type.render(), params.join(", ")) - } else { - format!("{}&", inner.render()) - } - } - - ResolvedType::Pointer(inner) => { - format!("{}*", inner.render()) - } - - ResolvedType::Reference(inner) => { - format!("{}&", inner.render()) - } - - ResolvedType::RValueReference(inner) => { - format!("{}&&", inner.render()) - } - - ResolvedType::Const(inner) => match inner.as_ref() { - ResolvedType::Pointer(pointee) => format!("{}*const", pointee.render()), - _ => format!("const {}", inner.render()), - }, - - ResolvedType::Volatile(inner) => { - format!("volatile {}", inner.render()) - } - - ResolvedType::Array { element, size } => match size { - Some(n) => format!("{}[{}]", element.render(), n), - None => format!("{}[]", element.render()), - }, - - ResolvedType::Unknown(s) => s.clone(), - - ResolvedType::Dependent(s) => s.clone(), - } - } - - pub fn render_for_display(&self) -> String { - normalize_pointer_reference_spacing(self.render()) - } -} - -pub fn render_type_for_display(original: &Type, resolved: &ResolvedType) -> String { - // Prefer source spelling only in carefully scoped cases (see helper below); - // otherwise use normalized rendering from semantic type model. - if should_prefer_source_display_name(original, resolved) { - original.get_display_name() - } else { - resolved.render_for_display() - } -} - -pub(crate) fn should_prefer_source_display_name(ty: &Type, resolved: &ResolvedType) -> bool { - // Source display names are used for externally declared/system types where - // canonicalized rendering may be less readable for users. - if !is_declared_in_external_or_system_header(ty) { - return false; - } - - // Keep template output stable/normalized via `ResolvedType` renderer. - if contains_template_type(resolved) { - return false; - } - - let source_display = ty.get_display_name(); - let rendered = resolved.render(); - - source_display != rendered -} - -fn is_declared_in_external_or_system_header(ty: &Type) -> bool { - ty.get_declaration() - .and_then(|decl| decl.get_location()) - .map(|location| { - let (path, _, _) = location.get_presumed_location(); - source_filter::is_external_or_system_path(&path) - }) - .unwrap_or(false) -} - -fn contains_template_type(resolved: &ResolvedType) -> bool { - match resolved { - ResolvedType::Template { .. } => true, - ResolvedType::Function { - return_type, - parameter_types, - .. - } => { - contains_template_type(return_type) - || parameter_types.iter().any(contains_template_type) - } - ResolvedType::FunctionPointer(inner) - | ResolvedType::FunctionReference(inner) - | ResolvedType::Pointer(inner) - | ResolvedType::Reference(inner) - | ResolvedType::RValueReference(inner) - | ResolvedType::Const(inner) - | ResolvedType::Volatile(inner) => contains_template_type(inner), - ResolvedType::Array { element, .. } => contains_template_type(element), - ResolvedType::Builtin(_) - | ResolvedType::UserDefined(_) - | ResolvedType::Unknown(_) - | ResolvedType::Dependent(_) => false, - } -} - -fn normalize_pointer_reference_spacing(type_name: String) -> String { - let chars: Vec = type_name.chars().collect(); - let mut out = String::with_capacity(type_name.len() + 8); - - for (idx, ch) in chars.iter().copied().enumerate() { - if is_pointer_or_reference(ch) && !is_function_pointer_marker(&chars, idx) { - insert_space_before_pointer_marker(&mut out, &chars, idx); - } - out.push(ch); - } - - out -} - -fn is_pointer_or_reference(ch: char) -> bool { - ch == '*' || ch == '&' -} - -fn is_function_pointer_marker(chars: &[char], idx: usize) -> bool { - matches!(chars.get(idx), Some('*' | '&')) - && matches!( - ( - idx.checked_sub(1).and_then(|i| chars.get(i)), - chars.get(idx + 1) - ), - (Some('('), Some(')')) - ) -} - -fn insert_space_before_pointer_marker(out: &mut String, chars: &[char], idx: usize) { - let prev_input = idx.checked_sub(1).and_then(|i| chars.get(i)).copied(); - - let is_first_pointer_in_sequence = prev_input != Some('*') && prev_input != Some('&'); - - if is_first_pointer_in_sequence && !out.ends_with(' ') && !out.ends_with('(') { - out.push(' '); - } -} - -pub fn resolve_type(original: &Type) -> ResolvedType { - // Resolve unqualified structural shape first, then re-apply top-level cv-qualifiers. - // This keeps qualifier placement consistent across all branches. - let canonical = original.get_canonical_type(); - let mut resolved = resolve_unqualified_type(original, &canonical); - - if original.is_const_qualified() { - resolved = ResolvedType::Const(Box::new(resolved)); - } - - if original.is_volatile_qualified() { - resolved = ResolvedType::Volatile(Box::new(resolved)); - } - - resolved -} - -fn resolve_unqualified_type(original: &Type, canonical: &Type) -> ResolvedType { - let kind = original.get_kind(); - - // Single source of truth for builtin mapping; extend here when adding builtin support. - if let Some(name) = builtin_name_from_type_kind(kind) { - return ResolvedType::Builtin(name.to_string()); - } - - match kind { - // ===== pointer ===== - TypeKind::Pointer => { - if let Some(inner) = original.get_pointee_type() { - let resolved_inner = resolve_type(&inner); - if matches!(resolved_inner, ResolvedType::Function { .. }) { - ResolvedType::FunctionPointer(Box::new(resolved_inner)) - } else { - ResolvedType::Pointer(Box::new(resolved_inner)) - } - } else { - unknown(original) - } - } - - // ===== reference ===== - TypeKind::LValueReference => { - if let Some(inner) = original.get_pointee_type() { - let resolved_inner = resolve_type(&inner); - if matches!(resolved_inner, ResolvedType::Function { .. }) { - ResolvedType::FunctionReference(Box::new(resolved_inner)) - } else { - ResolvedType::Reference(Box::new(resolved_inner)) - } - } else { - unknown(original) - } - } - - TypeKind::RValueReference => { - if let Some(inner) = original.get_pointee_type() { - let resolved_inner = resolve_type(&inner); - ResolvedType::RValueReference(Box::new(resolved_inner)) - } else { - unknown(original) - } - } - - // ===== function ===== - TypeKind::FunctionPrototype | TypeKind::FunctionNoPrototype => { - resolve_function_type(original) - } - - // ===== arrays ===== - TypeKind::ConstantArray => { - let element = original - .get_element_type() - .map(|t| resolve_type(&t)) - .unwrap_or_else(|| unknown(original)); - - ResolvedType::Array { - element: Box::new(element), - size: original.get_size(), - } - } - - // ===== user-defined / template ===== - // Named types (including aliases/templates) are resolved through decl-aware fallback. - _ => resolve_named_type(original, canonical), - } -} - -/// Maps clang `TypeKind` builtin kinds to canonical display names used in this model. -fn builtin_name_from_type_kind(kind: TypeKind) -> Option<&'static str> { - match kind { - TypeKind::Void => Some("void"), - TypeKind::Bool => Some("bool"), - TypeKind::CharS | TypeKind::SChar | TypeKind::UChar => Some("char"), - TypeKind::Short | TypeKind::UShort => Some("short"), - TypeKind::Int | TypeKind::UInt => Some("int"), - TypeKind::Long | TypeKind::ULong => Some("long"), - TypeKind::LongLong | TypeKind::ULongLong => Some("long long"), - TypeKind::Float => Some("float"), - TypeKind::Double => Some("double"), - _ => None, - } -} - -fn resolve_function_type(original: &Type) -> ResolvedType { - let return_type = original - .get_result_type() - .map(|t| resolve_type(&t)) - .unwrap_or_else(|| unknown(original)); - - let parameter_types = original - .get_argument_types() - .unwrap_or_default() - .into_iter() - .map(|arg| resolve_type(&arg)) - .collect(); - - ResolvedType::Function { - return_type: Box::new(return_type), - parameter_types, - is_variadic: original.is_variadic(), - } -} - -fn resolve_named_type(original: &Type, canonical: &Type) -> ResolvedType { - let display_name = original.get_display_name(); - let canonical_name = canonical.get_display_name(); - - // Heuristic: unqualified source name but qualified canonical name likely indicates - // alias/imported type; prefer canonical declaration path when possible. - if !display_name.contains("::") && canonical_name.contains("::") { - if let Some(resolved) = resolve_decl_based(canonical) { - return resolved; - } - } - - // For typedef/type-alias, canonical declaration usually yields stable target id. - // Exception: well-known system/STL aliases (e.g. `std::string`) canonicalize into - // deep, unreadable implementation-detail templates (`basic_string`) that - // no one writes in a design diagram -- keep just the alias's own name instead, - // ignoring any (possibly partially-defaulted) template arguments of its target. - if is_alias_type(original) { - if is_declared_in_external_or_system_header(original) { - if let Some(decl) = original.get_declaration() { - return ResolvedType::UserDefined(build_entity_id_from_decl(&decl)); - } - } else if let Some(resolved) = resolve_decl_based(canonical) { - return resolved; - } - } - - // Fallback order matters: - // 1) source declaration (preserves local spelling when available) - // 2) canonical declaration (captures normalized identity) - // 3) dependent-expression heuristic (e.g. `decltype(expr_using)` inside an - // uninstantiated template) — structurally unresolvable before instantiation - // 4) unknown name heuristic - if let Some(resolved) = resolve_decl_based(original) { - return resolved; - } - - if let Some(resolved) = resolve_decl_based(canonical) { - return resolved; - } - - if is_dependent_expression_type(original) { - let name = resolve_unknown_name(original, canonical); - log::debug!( - "type '{}' is structurally unresolvable before template instantiation \ - (dependent/decltype expression)", - name - ); - return ResolvedType::Dependent(name); - } - - let name = resolve_unknown_name(original, canonical); - log::debug!("could not resolve type '{}' to a concrete entity id", name); - ResolvedType::Unknown(name) -} - -/// Detects types libclang exposes as `Unexposed` because their meaning depends on -/// an unbound template parameter, e.g. `decltype(is_x_impl(std::declval()))` -/// in a template that is never instantiated in this translation unit. Such types -/// cannot be resolved to a concrete entity id without template instantiation, -/// which is out of scope for AST-only analysis. This is checked only after both -/// declaration-based resolution attempts have already failed, so it never shadows -/// a legitimately resolvable type. -fn is_dependent_expression_type(ty: &Type) -> bool { - ty.get_kind() == TypeKind::Unexposed -} - -fn resolve_unknown_name(original: &Type, canonical: &Type) -> String { - let display_name = original.get_display_name(); - let canonical_name = canonical.get_display_name(); - - // Prefer canonical only when it provides useful qualification and is not an - // implementation-detail placeholder (std::__*, type-parameter, auto-parameter). - if !display_name.contains("::") - && canonical_name.contains("::") - && !canonical_name.starts_with("std::__") - && !canonical_name.contains("type-parameter-") - && !canonical_name.contains("auto-parameter-") - { - canonical_name - } else { - display_name - } -} - -fn is_alias_type(ty: &Type) -> bool { - matches!( - ty.get_declaration().map(|decl| decl.get_kind()), - Some(EntityKind::TypedefDecl | EntityKind::TypeAliasDecl) - ) -} - -fn resolve_decl_based(ty: &Type) -> Option { - // Declaration-derived id is the primary identity source for user-defined types. - // Template arguments are recursively resolved into the same semantic model. - let decl = ty.get_declaration()?; - let entity_id = build_entity_id_from_decl(&decl); - - let args: Vec = ty - .get_template_argument_types() - .unwrap_or_default() - .into_iter() - .flatten() - .map(|arg_ty| resolve_type(&arg_ty)) - .collect(); - - if !args.is_empty() { - return Some(ResolvedType::Template { - base: entity_id, - args, - }); - } - - Some(ResolvedType::UserDefined(entity_id)) -} - -fn unknown(ty: &Type) -> ResolvedType { - ResolvedType::Unknown(ty.get_display_name()) -} - -fn build_entity_id_from_decl(entity: &Entity) -> String { - if entity.get_kind() == EntityKind::TemplateTemplateParameter { - return entity.get_name().unwrap_or_default(); - } - - strip_global_scope_prefix(&build_fqn_from_entity(entity)) -} - -fn strip_global_scope_prefix(type_name: &str) -> String { - type_name.trim_start_matches("::").to_string() -} - -pub(crate) fn collapse_std_internal_namespaces(parts: Vec<(String, bool)>) -> Vec { - let mut collapsed: Vec = Vec::with_capacity(parts.len()); - - for (name, is_namespace) in parts { - let prev = collapsed.last().map(|s| s.as_str()); - if should_skip_std_internal_namespace(is_namespace, &name, prev) { - continue; - } - collapsed.push(name); - } - - collapsed -} - -fn should_skip_std_internal_namespace( - is_namespace: bool, - name: &str, - previous_segment: Option<&str>, -) -> bool { - is_namespace && previous_segment == Some("std") && is_std_internal_namespace_segment(name) -} - -fn is_std_internal_namespace_segment(name: &str) -> bool { - name.strip_prefix("__") - .map(|rest| !rest.is_empty() && rest.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')) - .unwrap_or(false) -} - -/// Walk semantic parents of an entity to produce `Namespace::Class::Name`. -fn build_fqn_from_entity(entity: &Entity) -> String { - // Traversal is semantic (not lexical) so aliases/nested constructs resolve to - // stable ownership hierarchy used by relationship and id matching. - let mut parts: Vec<(String, bool)> = Vec::new(); - let mut current = Some(*entity); - - while let Some(entity) = current { - match entity.get_kind() { - EntityKind::Namespace => { - if let Some(name) = entity.get_name() { - parts.push((name, true)); - } - } - EntityKind::ClassDecl - | EntityKind::StructDecl - | EntityKind::UnionDecl - | EntityKind::EnumDecl - | EntityKind::ClassTemplate - | EntityKind::TemplateTemplateParameter - | EntityKind::TypedefDecl - | EntityKind::TypeAliasDecl => { - if let Some(name) = entity.get_name() { - parts.push((name, false)); - } - } - // Stop at TranslationUnit, unexposed decls, or anything else - _ => break, - } - current = entity.get_semantic_parent(); - } - - parts.reverse(); - collapse_std_internal_namespaces(parts).join("::") -} diff --git a/cpp/libclang/src/visitor/src/class_parser_helper_test.rs b/cpp/libclang/src/visitor/src/class_parser_helper_test.rs deleted file mode 100644 index af8ebc19..00000000 --- a/cpp/libclang/src/visitor/src/class_parser_helper_test.rs +++ /dev/null @@ -1,251 +0,0 @@ -// ******************************************************************************* -// 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 -// ******************************************************************************* - -#[path = "class_parser_helper.rs"] -mod class_parser_helper; -#[path = "source_filter.rs"] -mod source_filter; - -use class_parser_helper::{collapse_std_internal_namespaces, ResolvedType}; - -#[test] -fn referenced_entity_through_wrappers_points_to_inner_type() { - let ty = ResolvedType::Const(Box::new(ResolvedType::Pointer(Box::new( - ResolvedType::UserDefined("Vehicle::Engine".to_string()), - )))); - - assert_eq!(ty.referenced_entity_id(), Some("Vehicle::Engine")); -} - -#[test] -fn referenced_entity_through_function_pointer_chain() { - let ty = ResolvedType::FunctionPointer(Box::new(ResolvedType::Const(Box::new( - ResolvedType::Pointer(Box::new(ResolvedType::UserDefined("Engine".to_string()))), - )))); - - assert_eq!(ty.referenced_entity_id(), Some("Engine")); -} - -#[test] -fn referenced_entity_for_template_uses_template_base() { - let ty = ResolvedType::Reference(Box::new(ResolvedType::Template { - base: "std::vector".to_string(), - args: vec![ResolvedType::UserDefined("Vehicle::Engine".to_string())], - })); - - assert_eq!(ty.referenced_entity_id(), Some("std::vector")); -} - -#[test] -fn relationship_target_prefers_template_argument_over_base() { - let ty = ResolvedType::Template { - base: "std::vector".to_string(), - args: vec![ResolvedType::UserDefined("Vehicle::Engine".to_string())], - }; - - assert_eq!(ty.relationship_target_entity_id(), Some("Vehicle::Engine")); -} - -#[test] -fn render_template_pointer_and_array_types() { - let template = ResolvedType::Template { - base: "std::vector".to_string(), - args: vec![ResolvedType::UserDefined("MyNamespace::Engine".to_string())], - }; - assert_eq!( - template.render_for_display(), - "std::vector" - ); - - let ptr = ResolvedType::Pointer(Box::new(ResolvedType::UserDefined( - "MyNamespace::Engine".to_string(), - ))); - assert_eq!(ptr.render_for_display(), "MyNamespace::Engine *"); - - let arr = ResolvedType::Array { - element: Box::new(ResolvedType::Builtin("int".to_string())), - size: Some(8), - }; - assert_eq!(arr.render_for_display(), "int[8]"); -} - -#[test] -fn render_unsized_array_type() { - let arr = ResolvedType::Array { - element: Box::new(ResolvedType::Builtin("int".to_string())), - size: None, - }; - - assert_eq!(arr.render_for_display(), "int[]"); -} - -#[test] -fn render_const_pointer_type() { - let ty = ResolvedType::Const(Box::new(ResolvedType::Pointer(Box::new( - ResolvedType::Builtin("int".to_string()), - )))); - - assert_eq!(ty.render_for_display(), "int *const"); -} - -#[test] -fn render_volatile_type() { - let ty = ResolvedType::Volatile(Box::new(ResolvedType::Builtin("int".to_string()))); - - assert_eq!(ty.render_for_display(), "volatile int"); -} - -#[test] -fn render_function_and_function_pointer_types() { - let function = ResolvedType::Function { - return_type: Box::new(ResolvedType::Builtin("void".to_string())), - parameter_types: vec![ResolvedType::UserDefined("Engine".to_string())], - is_variadic: false, - }; - assert_eq!(function.render_for_display(), "void(Engine)"); - - let fn_ptr = ResolvedType::FunctionPointer(Box::new(function)); - assert_eq!(fn_ptr.render_for_display(), "void (*)(Engine)"); -} - -#[test] -fn render_function_reference_type() { - let function = ResolvedType::Function { - return_type: Box::new(ResolvedType::Builtin("void".to_string())), - parameter_types: vec![ResolvedType::UserDefined("Engine".to_string())], - is_variadic: false, - }; - - let fn_ref = ResolvedType::FunctionReference(Box::new(function)); - assert_eq!(fn_ref.render_for_display(), "void (&)(Engine)"); -} - -#[test] -fn render_variadic_function_return_type_style() { - let fn_type = ResolvedType::Function { - return_type: Box::new(ResolvedType::UserDefined( - "flatbuffers::FlatBufferBuilder".to_string(), - )), - parameter_types: vec![ResolvedType::Builtin("int".to_string())], - is_variadic: true, - }; - - assert_eq!( - fn_type.render_for_display(), - "flatbuffers::FlatBufferBuilder(int, ...)" - ); -} - -#[test] -fn non_owning_detection_for_pointer_and_const_pointer() { - let ptr = ResolvedType::Pointer(Box::new(ResolvedType::UserDefined("Engine".to_string()))); - assert!(ptr.is_non_owning()); - - let const_ptr = ResolvedType::Const(Box::new(ptr)); - assert!(const_ptr.is_non_owning()); -} - -#[test] -fn non_owning_detection_for_template_wrappers() { - let shared_ptr = ResolvedType::Template { - base: "std::shared_ptr".to_string(), - args: vec![ResolvedType::UserDefined("Engine".to_string())], - }; - assert!(shared_ptr.is_non_owning()); - - let vector_value = ResolvedType::Template { - base: "std::vector".to_string(), - args: vec![ResolvedType::UserDefined("Engine".to_string())], - }; - assert!(!vector_value.is_non_owning()); -} - -#[test] -fn collapse_std_internal_namespaces_only_under_std() { - let parts = vec![ - ("std".to_string(), true), - ("__1".to_string(), true), - ("vector".to_string(), false), - ]; - - assert_eq!( - collapse_std_internal_namespaces(parts), - vec!["std".to_string(), "vector".to_string()] - ); -} - -#[test] -fn collapse_std_internal_namespaces_does_not_drop_non_std_internal_namespaces() { - let parts = vec![ - ("foo".to_string(), true), - ("__detail".to_string(), true), - ("Bar".to_string(), false), - ]; - - assert_eq!( - collapse_std_internal_namespaces(parts), - vec!["foo".to_string(), "__detail".to_string(), "Bar".to_string()] - ); -} - -// `ResolvedType::Dependent` represents types libclang cannot resolve without -// template instantiation, e.g. the base class in: -// template -// struct is_maplike_container : decltype(is_maplike_container_impl(std::declval())) {}; -// It must behave like `Unknown` for entity/relationship lookups (there is no -// entity id to find), but is a distinct variant so callers can tell "expected, -// permanent limitation" apart from "missing/unanalyzed dependency" (`Unknown`). - -#[test] -fn dependent_type_has_no_referenced_entity_id() { - let ty = ResolvedType::Dependent( - "decltype(is_maplike_container_impl(std::declval()))".to_string(), - ); - - assert_eq!(ty.referenced_entity_id(), None); -} - -#[test] -fn dependent_type_has_no_relationship_target() { - let ty = ResolvedType::Dependent( - "decltype(is_maplike_container_impl(std::declval()))".to_string(), - ); - - assert_eq!(ty.relationship_target_entity_id(), None); -} - -#[test] -fn dependent_type_is_not_non_owning() { - let ty = ResolvedType::Dependent("decltype(foo(std::declval()))".to_string()); - - assert!(!ty.is_non_owning()); -} - -#[test] -fn dependent_type_renders_its_source_text() { - let ty = ResolvedType::Dependent("decltype(foo(std::declval()))".to_string()); - - assert_eq!(ty.render_for_display(), "decltype(foo(std::declval()))"); -} - -#[test] -fn dependent_type_nested_in_wrapper_has_no_referenced_entity_id() { - // A dependent expression wrapped in a qualifier (e.g. as a const base or - // through recursive resolution) must still be unresolvable, not silently - // fall back to `self` and look like something concrete. - let ty = ResolvedType::Const(Box::new(ResolvedType::Dependent( - "decltype(foo(std::declval()))".to_string(), - ))); - - assert_eq!(ty.referenced_entity_id(), None); -} diff --git a/cpp/libclang/src/visitor/src/class_relationship_resolver.rs b/cpp/libclang/src/visitor/src/class_relationship_resolver.rs new file mode 100644 index 00000000..eda98b9b --- /dev/null +++ b/cpp/libclang/src/visitor/src/class_relationship_resolver.rs @@ -0,0 +1,445 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Second-pass relationship inference for extracted class-diagram entities. + +use std::collections::HashSet; + +use class_diagram::{EntityType, RelationType, Relationship, SimpleEntity, SourceLocation}; +use cpp_semantics::ResolvedType; + +use crate::context::{ParsedClassInfo, ParsedMethodType, ParsedVariableType, VisitContext}; + +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 { + build_relationships_for_class(ctx, &builder); + infer_relationships_from_builder(ctx, &builder, &known_type_ids); + } +} + +fn build_relationships_for_class(ctx: &mut VisitContext, builder: &ParsedClassInfo) { + for base in &builder.base_classes { + let Some(resolved_base) = base.resolved_type.referenced_entity_id() else { + if matches!(base.resolved_type, ResolvedType::Dependent(_)) { + log::debug!( + "unable to resolve base type '{}' for '{}'; \ + skipping inheritance relationship (dependent/decltype expression)", + base.resolved_type.render_for_display(), + builder.id + ); + } else { + log::warn!( + "unable to resolve base type '{}' for '{}'; \ + skipping inheritance relationship (unexpected unresolved type)", + base.resolved_type.render_for_display(), + builder.id + ); + } + continue; + }; + + let Some(target_class) = ctx.types.get(resolved_base) else { + log::debug!( + "base type '{}' not found in type map for '{}'; \ + skipping inheritance relationship (external dependency)", + resolved_base, + builder.id + ); + continue; + }; + + let relation_type = if target_class.entity_type == EntityType::Interface { + RelationType::Implementation + } else { + RelationType::Inheritance + }; + + let Some(class) = ctx.types.get_mut(&builder.id) else { + log::warn!( + "source class '{}' unexpectedly missing from type map; \ + skipping inheritance relationship to '{}'", + builder.id, + resolved_base + ); + continue; + }; + + add_relationship( + class, + resolved_base.to_string(), + relation_type, + &base.source_location, + ); + } +} + +fn add_relationship( + class: &mut SimpleEntity, + target: String, + relation_type: RelationType, + source_location: &SourceLocation, +) { + if target == class.id { + return; + } + + let relationship = Relationship { + source: class.id.clone(), + target, + relation_type, + source_multiplicity: None, + target_multiplicity: None, + source_location: source_location.clone(), + }; + + let duplicate = class.relationships.iter().any(|existing| { + existing.source == relationship.source + && existing.target == relationship.target + && existing.relation_type == relationship.relation_type + && existing.source_multiplicity == relationship.source_multiplicity + && existing.target_multiplicity == relationship.target_multiplicity + }); + + if !duplicate { + class.relationships.push(relationship); + } +} + +fn infer_relationships_from_builder( + ctx: &mut VisitContext, + builder: &ParsedClassInfo, + known_class_ids: &HashSet, +) { + let Some(class) = ctx.types.get_mut(&builder.id) else { + log::warn!( + "source class '{}' unexpectedly missing from type map; \ + skipping inferred relationships", + builder.id + ); + return; + }; + + infer_variable_relationships(class, &builder.variable_types, known_class_ids); + infer_method_relationships(class, &builder.method_types, known_class_ids); +} + +fn infer_variable_relationships( + class: &mut SimpleEntity, + variable_types: &[ParsedVariableType], + known_class_ids: &HashSet, +) { + for variable in variable_types { + add_relationship_from_resolved_type( + class, + &variable.resolved_type, + known_class_ids, + RelationType::Aggregation, + RelationType::Composition, + &variable.source_location, + ); + } +} + +fn infer_method_relationships( + class: &mut SimpleEntity, + method_types: &[ParsedMethodType], + known_class_ids: &HashSet, +) { + for method in method_types { + add_relationship_from_resolved_type( + class, + &method.return_type, + known_class_ids, + RelationType::Dependency, + RelationType::Association, + &method.source_location, + ); + + for parameter_type in &method.parameter_types { + add_relationship_from_resolved_type( + class, + parameter_type, + known_class_ids, + RelationType::Dependency, + RelationType::Association, + &method.source_location, + ); + } + } +} + +fn add_relationship_from_resolved_type( + class: &mut SimpleEntity, + resolved_type: &ResolvedType, + known_class_ids: &HashSet, + non_owning_relation: RelationType, + owning_relation: RelationType, + source_location: &SourceLocation, +) { + let Some(raw_target) = resolved_type.relationship_target_entity_id() else { + return; + }; + + let Some(target) = resolve_in_model_target(class, raw_target, known_class_ids) else { + return; + }; + + let relation_type = if resolved_type.is_non_owning() { + non_owning_relation + } else { + owning_relation + }; + + add_relationship(class, target, relation_type, source_location); +} + +fn resolve_in_model_target( + source_class: &SimpleEntity, + raw_target: &str, + known_class_ids: &HashSet, +) -> Option { + if known_class_ids.contains(raw_target) { + return Some(raw_target.to_string()); + } + + if !raw_target.contains("::") { + if let Some(namespace) = source_class.enclosing_namespace_id.as_deref() { + let mut current_namespace: Option<&str> = Some(namespace); + while let Some(current_namespace_id) = current_namespace { + let candidate = format!("{current_namespace_id}::{raw_target}"); + if known_class_ids.contains(&candidate) { + return Some(candidate); + } + current_namespace = current_namespace_id + .rsplit_once("::") + .map(|(parent, _)| parent); + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use class_diagram::{RelationType, SimpleEntity, SourceLocation}; + use cpp_semantics::ResolvedType; + + use super::resolve_relationships; + use crate::context::{ + ParsedBaseClass, ParsedClassInfo, ParsedMethodType, ParsedVariableType, VisitContext, + }; + + #[test] + fn resolve_relationships_uses_variable_and_method_source_locations() { + let source_file = "unit_source.cpp"; + + let mut ctx = VisitContext::default(); + ctx.types.insert( + "Engine".to_string(), + SimpleEntity { + id: "Engine".to_string(), + name: "Engine".to_string(), + source_location: SourceLocation::new(source_file, 1), + ..Default::default() + }, + ); + ctx.types.insert( + "Car".to_string(), + SimpleEntity { + id: "Car".to_string(), + name: "Car".to_string(), + source_location: SourceLocation::new(source_file, 3), + ..Default::default() + }, + ); + + 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), + }], + }); + + resolve_relationships(&mut ctx); + + let car = ctx + .types + .get("Car") + .expect("Car must still exist after relationship resolution"); + + // Class source location should not be modified by relationship resolution. + assert_eq!(car.source_location, SourceLocation::new(source_file, 3)); + + let variable_relationship = car + .relationships + .iter() + .find(|relationship| { + relationship.target == "Engine" + && relationship.relation_type == RelationType::Composition + }) + .expect("Expected a composition relationship inferred from member variable type"); + assert_eq!( + variable_relationship.source_location, + SourceLocation::new(source_file, 5) + ); + + let method_relationship = car + .relationships + .iter() + .find(|relationship| { + relationship.target == "Engine" + && relationship.relation_type == RelationType::Association + }) + .expect("Expected an association relationship inferred from method return type"); + assert_eq!( + method_relationship.source_location, + SourceLocation::new(source_file, 6) + ); + } + + /// Regression test for a real crash: a base class like + /// `struct is_maplike_container : decltype(is_maplike_container_impl(std::declval())) {};` + /// resolves to `ResolvedType::Dependent(..)` because `decltype(...)` of a + /// dependent expression cannot be tied to a concrete entity id without template + /// instantiation. `resolve_relationships` must not panic on this: it should skip + /// only the unresolvable base while still building relationships for any other, + /// resolvable base classes on the same type. + #[test] + fn resolve_relationships_skips_dependent_base_class_without_panicking() { + let source_file = "is_maplike_container.hpp"; + let mut ctx = VisitContext::default(); + ctx.types.insert( + "amp::detail::is_container_base".to_string(), + SimpleEntity { + id: "amp::detail::is_container_base".to_string(), + name: "is_container_base".to_string(), + source_location: SourceLocation::new(source_file, 1), + ..Default::default() + }, + ); + ctx.types.insert( + "amp::detail::is_maplike_container".to_string(), + SimpleEntity { + id: "amp::detail::is_maplike_container".to_string(), + name: "is_maplike_container".to_string(), + source_location: SourceLocation::new(source_file, 5), + ..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![], + }); + + // Must not panic. + resolve_relationships(&mut ctx); + + let is_maplike_container = ctx + .types + .get("amp::detail::is_maplike_container") + .expect("is_maplike_container must still exist after relationship resolution"); + + // No relationship should have been created for the unresolvable dependent base. + assert!( + !is_maplike_container + .relationships + .iter() + .any(|relationship| relationship.relation_type == RelationType::Implementation), + "dependent base class must not produce a relationship" + ); + + // The sibling resolvable base class must still be processed correctly. + let inheritance_relationship = is_maplike_container + .relationships + .iter() + .find(|relationship| { + relationship.target == "amp::detail::is_container_base" + && relationship.relation_type == RelationType::Inheritance + }) + .expect("Expected an inheritance relationship for the resolvable base class"); + assert_eq!( + inheritance_relationship.source_location, + SourceLocation::new(source_file, 5) + ); + } + + /// An unresolved base type that is *not* `ResolvedType::Dependent` (e.g. + /// `Unknown`) is unexpected and gets a `log::warn!`, but must still never + /// abort the parser — when in doubt, warn and skip rather than crash. + #[test] + fn resolve_relationships_warns_and_skips_unexpected_unresolved_base() { + let source_file = "unit_source.cpp"; + let mut ctx = VisitContext::default(); + ctx.types.insert( + "Derived".to_string(), + SimpleEntity { + id: "Derived".to_string(), + name: "Derived".to_string(), + source_location: SourceLocation::new(source_file, 1), + ..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![], + }); + + // Must not panic. + resolve_relationships(&mut ctx); + + let derived = ctx + .types + .get("Derived") + .expect("Derived must still exist after relationship resolution"); + assert!( + derived.relationships.is_empty(), + "unresolvable base class must not produce a relationship" + ); + } +} diff --git a/cpp/libclang/src/visitor/src/class_visitor.rs b/cpp/libclang/src/visitor/src/class_visitor.rs index f614011e..ecf701d6 100644 --- a/cpp/libclang/src/visitor/src/class_visitor.rs +++ b/cpp/libclang/src/visitor/src/class_visitor.rs @@ -12,31 +12,36 @@ // ******************************************************************************* use clang::{Entity, EntityKind}; -use std::collections::HashSet; use class_diagram::{ - EntityType, FunctionArgument, MemberVariable, Method, MethodModifier, RelationType, - Relationship, SimpleEntity, SourceLocation, TemplateParameter, TypeAlias, Visibility, + EntityType, FunctionArgument, MemberVariable, Method, MethodModifier, SimpleEntity, + TemplateParameter, TypeAlias, Visibility, }; +use cpp_semantics::ResolvedType; -use crate::class_parser_helper::{render_type_for_display, resolve_type, ResolvedType}; +use crate::clang_adapter::scope::namespace_id; +use crate::clang_adapter::source_location::parse_source_location; use crate::context::{ ParsedBaseClass, ParsedClassInfo, ParsedMethodType, ParsedVariableType, VisitContext, }; +use crate::types::renderer::render_type_for_display; +use crate::types::resolver::resolve_type; use crate::visitor::AstVisitor; pub struct ClassVisitor; impl AstVisitor for ClassVisitor { fn visit(ctx: &mut VisitContext, entity: Entity) { - let template_params = if ctx.is_templated { - parse_template_parameters(&entity) - } else { - None + let template_params = match entity.get_kind() { + EntityKind::ClassTemplate | EntityKind::ClassTemplatePartialSpecialization => { + parse_template_parameters(&entity) + } + _ => None, }; - let namespace = Self::get_namespace_id(&entity); + let namespace = namespace_id(&entity); - if let Some((builder, mut class_entity)) = Self::visit_class(&entity, namespace.as_deref()) + if let Some((builder, mut class_entity)) = + Self::visit_class(&entity, namespace.as_deref()) { class_entity.template_parameters = template_params; ctx.parsed_class_info.push(builder); @@ -46,14 +51,10 @@ impl AstVisitor for ClassVisitor { } impl ClassVisitor { + /// Compatibility entry point for callers that previously invoked the class visitor's + /// relationship phase directly. pub 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 { - build_relationships_for_class(ctx, &builder); - infer_relationships_from_builder(ctx, &builder, &known_type_ids); - } + crate::class_relationship_resolver::resolve_relationships(ctx); } fn visit_class( @@ -157,18 +158,6 @@ fn class_entity_id(entity: &Entity, namespace: Option<&str>, name: &str) -> Stri } } -pub(crate) fn parse_source_location(entity: &Entity) -> SourceLocation { - let Some(location) = entity.get_location() else { - return SourceLocation::default(); - }; - - let file_location = location.get_file_location(); - let source_file = file_location - .file - .map(|f| f.get_path().to_string_lossy().to_string()); - SourceLocation::new(source_file.unwrap_or_default(), file_location.line) -} - fn collect_variable_type(entity: &Entity) -> Option { let Some(name) = entity.get_name() else { log::debug!("skipping field/variable: entity has no name"); @@ -437,216 +426,3 @@ fn infer_entity_type_from_members(kind: EntityKind, class: &SimpleEntity) -> Ent EntityType::Class } } - -// Relationship part -fn build_relationships_for_class(ctx: &mut VisitContext, builder: &ParsedClassInfo) { - for base in &builder.base_classes { - let Some(resolved_base) = base.resolved_type.referenced_entity_id() else { - if matches!(base.resolved_type, ResolvedType::Dependent(_)) { - // Expected, permanent limitation of AST-only analysis (e.g. a - // `decltype`/SFINAE base class that cannot be resolved without - // template instantiation) — never abort, not even in debug/test - // builds. - log::debug!( - "unable to resolve base type '{}' for '{}'; \ - skipping inheritance relationship (dependent/decltype expression)", - base.resolved_type.render_for_display(), - builder.id - ); - } else { - // Unexpected: a base class resolving to `Unknown`/`Builtin` likely - // indicates a gap in the resolver rather than a known limitation. - // When in doubt, warn and continue rather than abort — a single - // unanticipated input must never crash the parser. - log::warn!( - "unable to resolve base type '{}' for '{}'; \ - skipping inheritance relationship (unexpected unresolved type)", - base.resolved_type.render_for_display(), - builder.id - ); - } - continue; - }; - - let Some(target_class) = ctx.types.get(resolved_base) else { - // Base type is not in the type map — it is likely an external dependency - // that was filtered out during the visit phase. This is expected and - // common, so skip the relationship without ever aborting. - log::debug!( - "base type '{}' not found in type map for '{}'; \ - skipping inheritance relationship (external dependency)", - resolved_base, - builder.id - ); - continue; - }; - - let relation_type = if target_class.entity_type == EntityType::Interface { - RelationType::Implementation - } else { - RelationType::Inheritance - }; - - let Some(class) = ctx.types.get_mut(&builder.id) else { - // Internal invariant: `builder.id` is derived from `ctx.types` during - // the visit phase, so it should always still be present here. If it - // isn't, that's a bug in the visitor pipeline rather than an expected - // input condition. When in doubt, warn and skip rather than abort — - // a single unanticipated input must never crash the parser. - log::warn!( - "source class '{}' unexpectedly missing from type map; \ - skipping inheritance relationship to '{}'", - builder.id, - resolved_base - ); - continue; - }; - - add_relationship( - class, - resolved_base.to_string(), - relation_type, - &base.source_location, - ); - } -} - -fn add_relationship( - class: &mut SimpleEntity, - target: String, - relation_type: RelationType, - source_location: &SourceLocation, -) { - if target == class.id { - return; - } - - let relationship = Relationship { - source: class.id.clone(), - target, - relation_type, - source_multiplicity: None, - target_multiplicity: None, - source_location: source_location.clone(), - }; - - let duplicate = class.relationships.iter().any(|existing| { - existing.source == relationship.source - && existing.target == relationship.target - && existing.relation_type == relationship.relation_type - && existing.source_multiplicity == relationship.source_multiplicity - && existing.target_multiplicity == relationship.target_multiplicity - }); - - if !duplicate { - class.relationships.push(relationship); - } -} - -fn infer_relationships_from_builder( - ctx: &mut VisitContext, - builder: &ParsedClassInfo, - known_class_ids: &HashSet, -) { - let Some(class) = ctx.types.get_mut(&builder.id) else { - return; - }; - - infer_variable_relationships(class, &builder.variable_types, known_class_ids); - infer_method_relationships(class, &builder.method_types, known_class_ids); -} - -fn infer_variable_relationships( - class: &mut SimpleEntity, - variable_types: &[ParsedVariableType], - known_class_ids: &HashSet, -) { - for variable in variable_types { - add_relationship_from_resolved_type( - class, - &variable.resolved_type, - known_class_ids, - RelationType::Aggregation, - RelationType::Composition, - &variable.source_location, - ); - } -} - -fn infer_method_relationships( - class: &mut SimpleEntity, - method_types: &[ParsedMethodType], - known_class_ids: &HashSet, -) { - for method in method_types { - add_relationship_from_resolved_type( - class, - &method.return_type, - known_class_ids, - RelationType::Dependency, - RelationType::Association, - &method.source_location, - ); - - for parameter_type in &method.parameter_types { - add_relationship_from_resolved_type( - class, - parameter_type, - known_class_ids, - RelationType::Dependency, - RelationType::Association, - &method.source_location, - ); - } - } -} - -fn add_relationship_from_resolved_type( - class: &mut SimpleEntity, - resolved_type: &ResolvedType, - known_class_ids: &HashSet, - non_owning_relation: RelationType, - owning_relation: RelationType, - source_location: &SourceLocation, -) { - let Some(raw_target) = resolved_type.relationship_target_entity_id() else { - return; - }; - - let Some(target) = resolve_in_model_target(class, raw_target, known_class_ids) else { - return; - }; - - let relation_type = if resolved_type.is_non_owning() { - non_owning_relation - } else { - owning_relation - }; - - add_relationship(class, target, relation_type, source_location); -} - -fn resolve_in_model_target( - source_class: &SimpleEntity, - raw_target: &str, - known_class_ids: &HashSet, -) -> Option { - if known_class_ids.contains(raw_target) { - return Some(raw_target.to_string()); - } - - if !raw_target.contains("::") { - if let Some(ns) = source_class.enclosing_namespace_id.as_deref() { - let mut current_ns: Option<&str> = Some(ns); - while let Some(n) = current_ns { - let candidate = format!("{n}::{raw_target}"); - if known_class_ids.contains(&candidate) { - return Some(candidate); - } - current_ns = n.rsplit_once("::").map(|(parent, _)| parent); - } - } - } - - None -} diff --git a/cpp/libclang/src/visitor/src/class_visitor_test.rs b/cpp/libclang/src/visitor/src/class_visitor_test.rs deleted file mode 100644 index 6968cead..00000000 --- a/cpp/libclang/src/visitor/src/class_visitor_test.rs +++ /dev/null @@ -1,225 +0,0 @@ -// ******************************************************************************* -// 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 class_diagram::{RelationType, SimpleEntity, SourceLocation}; -use visit_tu::context::{ - ParsedBaseClass, ParsedClassInfo, ParsedMethodType, ParsedVariableType, VisitContext, -}; -use visit_tu::{ClassVisitor, ResolvedType}; - -#[test] -fn resolve_relationships_uses_variable_and_method_source_locations() { - let source_file = "unit_source.cpp"; - - let mut ctx = VisitContext::default(); - ctx.types.insert( - "Engine".to_string(), - SimpleEntity { - id: "Engine".to_string(), - name: "Engine".to_string(), - source_location: SourceLocation::new(source_file, 1), - ..Default::default() - }, - ); - ctx.types.insert( - "Car".to_string(), - SimpleEntity { - id: "Car".to_string(), - name: "Car".to_string(), - source_location: SourceLocation::new(source_file, 3), - ..Default::default() - }, - ); - - 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), - }], - }); - - ClassVisitor::resolve_relationships(&mut ctx); - - let car = ctx - .types - .get("Car") - .expect("Car must still exist after relationship resolution"); - - // Class source location should not be modified by relationship resolution. - assert_eq!(car.source_location, SourceLocation::new(source_file, 3)); - - let variable_relationship = car - .relationships - .iter() - .find(|relationship| { - relationship.target == "Engine" - && relationship.relation_type == RelationType::Composition - }) - .expect("Expected a composition relationship inferred from member variable type"); - - assert_eq!( - variable_relationship.source_location, - SourceLocation::new(source_file, 5) - ); - - let method_relationship = car - .relationships - .iter() - .find(|relationship| { - relationship.target == "Engine" - && relationship.relation_type == RelationType::Association - }) - .expect("Expected an association relationship inferred from method return type"); - - assert_eq!( - method_relationship.source_location, - SourceLocation::new(source_file, 6) - ); -} - -/// Regression test for a real crash: a base class like -/// `struct is_maplike_container : decltype(is_maplike_container_impl(std::declval())) {};` -/// resolves to `ResolvedType::Dependent(..)` because `decltype(...)` of a -/// dependent expression cannot be tied to a concrete entity id without template -/// instantiation. `resolve_relationships` must not panic on this: it should skip -/// only the unresolvable base while still building relationships for any other, -/// resolvable base classes on the same type. -#[test] -fn resolve_relationships_skips_dependent_base_class_without_panicking() { - let source_file = "is_maplike_container.hpp"; - - let mut ctx = VisitContext::default(); - ctx.types.insert( - "amp::detail::is_container_base".to_string(), - SimpleEntity { - id: "amp::detail::is_container_base".to_string(), - name: "is_container_base".to_string(), - source_location: SourceLocation::new(source_file, 1), - ..Default::default() - }, - ); - ctx.types.insert( - "amp::detail::is_maplike_container".to_string(), - SimpleEntity { - id: "amp::detail::is_maplike_container".to_string(), - name: "is_maplike_container".to_string(), - source_location: SourceLocation::new(source_file, 5), - ..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![], - }); - - // Must not panic. - ClassVisitor::resolve_relationships(&mut ctx); - - let is_maplike_container = ctx - .types - .get("amp::detail::is_maplike_container") - .expect("is_maplike_container must still exist after relationship resolution"); - - // No relationship should have been created for the unresolvable dependent base. - assert!( - !is_maplike_container - .relationships - .iter() - .any(|relationship| relationship.relation_type == RelationType::Implementation), - "dependent base class must not produce a relationship" - ); - - // The sibling resolvable base class must still be processed correctly. - let inheritance_relationship = is_maplike_container - .relationships - .iter() - .find(|relationship| { - relationship.target == "amp::detail::is_container_base" - && relationship.relation_type == RelationType::Inheritance - }) - .expect("Expected an inheritance relationship for the resolvable base class"); - - assert_eq!( - inheritance_relationship.source_location, - SourceLocation::new(source_file, 5) - ); -} - -/// An unresolved base type that is *not* `ResolvedType::Dependent` (e.g. -/// `Unknown`) is unexpected and gets a `log::warn!`, but must still never -/// abort the parser — when in doubt, warn and skip rather than crash. -#[test] -fn resolve_relationships_warns_and_skips_unexpected_unresolved_base() { - let source_file = "unit_source.cpp"; - - let mut ctx = VisitContext::default(); - ctx.types.insert( - "Derived".to_string(), - SimpleEntity { - id: "Derived".to_string(), - name: "Derived".to_string(), - source_location: SourceLocation::new(source_file, 1), - ..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![], - }); - - // Must not panic. - ClassVisitor::resolve_relationships(&mut ctx); - - let derived = ctx - .types - .get("Derived") - .expect("Derived must still exist after relationship resolution"); - assert!( - derived.relationships.is_empty(), - "unresolvable base class must not produce a relationship" - ); -} diff --git a/cpp/libclang/src/visitor/src/context.rs b/cpp/libclang/src/visitor/src/context.rs index bc4f4911..2e2b1be5 100644 --- a/cpp/libclang/src/visitor/src/context.rs +++ b/cpp/libclang/src/visitor/src/context.rs @@ -11,13 +11,12 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use class_diagram::{SimpleEntity, SourceLocation}; +use cpp_semantics::ResolvedType; use sequence_logic::FunctionDef; - -use crate::class_parser_helper::ResolvedType; +use serde::{Deserialize, Serialize}; pub type TypeMap = HashMap; @@ -26,7 +25,6 @@ pub struct VisitContext { pub types: TypeMap, pub parsed_class_info: Vec, pub functions: Vec, - pub is_templated: bool, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] diff --git a/cpp/libclang/src/visitor/src/enum_visitor.rs b/cpp/libclang/src/visitor/src/enum_visitor.rs index e359a470..b8ebc51c 100644 --- a/cpp/libclang/src/visitor/src/enum_visitor.rs +++ b/cpp/libclang/src/visitor/src/enum_visitor.rs @@ -11,12 +11,14 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -use crate::class_visitor::parse_source_location; -use crate::context::VisitContext; -use crate::visitor::AstVisitor; use clang::Entity; use class_diagram::{EntityType, EnumLiteral, SimpleEntity}; +use crate::clang_adapter::scope::namespace_id; +use crate::clang_adapter::source_location::parse_source_location; +use crate::context::VisitContext; +use crate::visitor::AstVisitor; + pub struct EnumVisitor; impl AstVisitor for EnumVisitor { @@ -33,7 +35,7 @@ impl EnumVisitor { log::debug!("skipping enum: anonymous enum has no name"); return None; }; - let namespace_id = Self::get_namespace_id(&entity); + let namespace_id = namespace_id(&entity); let full_qualified_id = if let Some(namespace_id) = &namespace_id { format!("{}::{}", namespace_id, name) } else { diff --git a/cpp/libclang/src/visitor/src/lib.rs b/cpp/libclang/src/visitor/src/lib.rs index ab762ad9..6b8d0bf2 100644 --- a/cpp/libclang/src/visitor/src/lib.rs +++ b/cpp/libclang/src/visitor/src/lib.rs @@ -11,20 +11,22 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -mod class_parser_helper; +mod clang_adapter; +mod class_relationship_resolver; mod class_visitor; pub mod context; mod enum_visitor; mod function_visitor; -mod source_filter; +mod types; pub mod visitor; -pub use class_parser_helper::ResolvedType; +pub use cpp_semantics::ResolvedType; +pub use sequence_logic::{BodyItem, FunctionDef}; + +pub use clang_adapter::source_filter::is_external_dependency_path; pub use class_visitor::ClassVisitor; pub use context::VisitContext; pub use enum_visitor::EnumVisitor; pub use function_visitor::FunctionVisitor; -pub use sequence_logic::{BodyItem, FunctionDef}; -pub use source_filter::is_external_dependency_path; pub use visitor::AstVisitor; pub use visitor::Visitor; diff --git a/cpp/libclang/src/visitor/src/types/mod.rs b/cpp/libclang/src/visitor/src/types/mod.rs new file mode 100644 index 00000000..ceaa7753 --- /dev/null +++ b/cpp/libclang/src/visitor/src/types/mod.rs @@ -0,0 +1,17 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Conversion and display rules for C++ types. + +pub(crate) mod renderer; +pub(crate) mod resolver; diff --git a/cpp/libclang/src/visitor/src/types/renderer.rs b/cpp/libclang/src/visitor/src/types/renderer.rs new file mode 100644 index 00000000..c00108c3 --- /dev/null +++ b/cpp/libclang/src/visitor/src/types/renderer.rs @@ -0,0 +1,70 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Presentation rules for resolved libclang types. + +use clang::Type; +use cpp_semantics::ResolvedType; + +use crate::clang_adapter::source_filter; + +pub(crate) fn render_type_for_display(original: &Type, resolved: &ResolvedType) -> String { + // Prefer source spelling only in carefully scoped cases (see helper below); + // otherwise use normalized rendering from semantic type model. + if should_prefer_source_display_name(original, resolved) { + original.get_display_name() + } else { + resolved.render_for_display() + } +} + +fn should_prefer_source_display_name(ty: &Type, resolved: &ResolvedType) -> bool { + // Source display names are used for externally declared/system types where + // canonicalized rendering may be less readable for users. + if !source_filter::is_declared_in_external_or_system_header(ty) + || contains_template_type(resolved) + { + return false; + } + + let source_display = ty.get_display_name(); + let rendered = resolved.render_for_display(); + + source_display != rendered +} + +fn contains_template_type(resolved: &ResolvedType) -> bool { + match resolved { + ResolvedType::Template { .. } => true, + ResolvedType::Function { + return_type, + parameter_types, + .. + } => { + contains_template_type(return_type) + || parameter_types.iter().any(contains_template_type) + } + ResolvedType::FunctionPointer(inner) + | ResolvedType::FunctionReference(inner) + | ResolvedType::Pointer(inner) + | ResolvedType::Reference(inner) + | ResolvedType::RValueReference(inner) + | ResolvedType::Const(inner) + | ResolvedType::Volatile(inner) => contains_template_type(inner), + ResolvedType::Array { element, .. } => contains_template_type(element), + ResolvedType::Builtin(_) + | ResolvedType::UserDefined(_) + | ResolvedType::Unknown(_) + | ResolvedType::Dependent(_) => false, + } +} diff --git a/cpp/libclang/src/visitor/src/types/resolver.rs b/cpp/libclang/src/visitor/src/types/resolver.rs new file mode 100644 index 00000000..f63969f3 --- /dev/null +++ b/cpp/libclang/src/visitor/src/types/resolver.rs @@ -0,0 +1,339 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +//! Conversion from libclang types to the C++ semantic type model. + +#![cfg_attr(test, allow(dead_code))] + +use clang::{Entity, EntityKind, Type, TypeKind}; +use cpp_semantics::ResolvedType; + +use crate::clang_adapter::source_filter; + +pub(crate) fn resolve_type(original: &Type) -> ResolvedType { + // Resolve unqualified structural shape first, then re-apply top-level cv-qualifiers. + // This keeps qualifier placement consistent across all branches. + let canonical = original.get_canonical_type(); + let mut resolved = resolve_unqualified_type(original, &canonical); + + if original.is_const_qualified() { + resolved = ResolvedType::Const(Box::new(resolved)); + } + if original.is_volatile_qualified() { + resolved = ResolvedType::Volatile(Box::new(resolved)); + } + resolved +} + +fn resolve_unqualified_type(original: &Type, canonical: &Type) -> ResolvedType { + // Single source of truth for builtin mapping; extend here when adding builtin support. + if let Some(name) = builtin_name(original.get_kind()) { + return ResolvedType::Builtin(name.to_string()); + } + + match original.get_kind() { + // ===== pointer ===== + TypeKind::Pointer => original + .get_pointee_type() + .map(|inner| match resolve_type(&inner) { + function @ ResolvedType::Function { .. } => { + ResolvedType::FunctionPointer(Box::new(function)) + } + inner => ResolvedType::Pointer(Box::new(inner)), + }) + .unwrap_or_else(|| unknown(original)), + + // ===== reference ===== + TypeKind::LValueReference => original + .get_pointee_type() + .map(|inner| match resolve_type(&inner) { + function @ ResolvedType::Function { .. } => { + ResolvedType::FunctionReference(Box::new(function)) + } + inner => ResolvedType::Reference(Box::new(inner)), + }) + .unwrap_or_else(|| unknown(original)), + TypeKind::RValueReference => original + .get_pointee_type() + .map(|inner| ResolvedType::RValueReference(Box::new(resolve_type(&inner)))) + .unwrap_or_else(|| unknown(original)), + + // ===== function ===== + TypeKind::FunctionPrototype | TypeKind::FunctionNoPrototype => { + resolve_function_type(original) + } + + // ===== arrays ===== + TypeKind::ConstantArray => ResolvedType::Array { + element: Box::new( + original + .get_element_type() + .map(|element| resolve_type(&element)) + .unwrap_or_else(|| unknown(original)), + ), + size: original.get_size(), + }, + + // ===== user-defined / template ===== + // Named types (including aliases/templates) are resolved through decl-aware fallback. + _ => resolve_named_type(original, canonical), + } +} + +/// Maps clang `TypeKind` builtin kinds to canonical display names used in this model. +fn builtin_name(kind: TypeKind) -> Option<&'static str> { + match kind { + TypeKind::Void => Some("void"), + TypeKind::Bool => Some("bool"), + TypeKind::CharS | TypeKind::SChar | TypeKind::UChar => Some("char"), + TypeKind::Short | TypeKind::UShort => Some("short"), + TypeKind::Int | TypeKind::UInt => Some("int"), + TypeKind::Long | TypeKind::ULong => Some("long"), + TypeKind::LongLong | TypeKind::ULongLong => Some("long long"), + TypeKind::Float => Some("float"), + TypeKind::Double => Some("double"), + _ => None, + } +} + +fn resolve_function_type(original: &Type) -> ResolvedType { + let return_type = original + .get_result_type() + .map(|ty| resolve_type(&ty)) + .unwrap_or_else(|| unknown(original)); + let parameter_types = original + .get_argument_types() + .unwrap_or_default() + .into_iter() + .map(|ty| resolve_type(&ty)) + .collect(); + + ResolvedType::Function { + return_type: Box::new(return_type), + parameter_types, + is_variadic: original.is_variadic(), + } +} + +fn resolve_named_type(original: &Type, canonical: &Type) -> ResolvedType { + let display_name = original.get_display_name(); + let canonical_name = canonical.get_display_name(); + + // For typedef/type-alias, canonical declaration usually yields stable target id. + // Exception: well-known system/STL aliases (e.g. `std::string`) canonicalize into + // deep, unreadable implementation-detail templates (`basic_string`) that + // no one writes in a design diagram -- keep just the alias's own name instead, + // ignoring any (possibly partially-defaulted) template arguments of its target. + if is_alias_type(original) { + if source_filter::is_declared_in_external_or_system_header(original) { + if let Some(declaration) = original.get_declaration() { + return ResolvedType::UserDefined(entity_id_from_decl(&declaration)); + } + } else if let Some(resolved) = resolve_decl_based(canonical) { + return resolved; + } + } + + // Heuristic: an unqualified non-alias source name with a qualified canonical name + // is likely an imported type; prefer the canonical declaration when possible. + // This runs after alias handling so an external alias cannot be replaced by an + // implementation-detail canonical type. + if !display_name.contains("::") && canonical_name.contains("::") { + if let Some(resolved) = resolve_decl_based(canonical) { + return resolved; + } + } + + // Fallback order matters: + // 1) source declaration (preserves local spelling when available) + // 2) canonical declaration (captures normalized identity) + // 3) dependent-expression heuristic (e.g. `decltype(expr_using)` inside an + // uninstantiated template) — structurally unresolvable before instantiation + // 4) unknown name heuristic + resolve_decl_based(original) + .or_else(|| resolve_decl_based(canonical)) + .unwrap_or_else(|| { + let name = resolve_unknown_name(original, canonical); + if is_dependent_expression_type(original) { + log::debug!( + "type '{}' is structurally unresolvable before template instantiation", + name + ); + ResolvedType::Dependent(name) + } else { + log::debug!("could not resolve type '{}' to a concrete entity id", name); + ResolvedType::Unknown(name) + } + }) +} + +/// Detects types libclang exposes as `Unexposed` because their meaning depends on +/// an unbound template parameter, e.g. `decltype(is_x_impl(std::declval()))` +/// in a template that is never instantiated in this translation unit. Such types +/// cannot be resolved to a concrete entity id without template instantiation, +/// which is out of scope for AST-only analysis. This is checked only after both +/// declaration-based resolution attempts have already failed, so it never shadows +/// a legitimately resolvable type. +fn is_dependent_expression_type(ty: &Type) -> bool { + ty.get_kind() == TypeKind::Unexposed +} + +fn resolve_unknown_name(original: &Type, canonical: &Type) -> String { + let display_name = original.get_display_name(); + let canonical_name = canonical.get_display_name(); + + // Prefer canonical only when it provides useful qualification and is not an + // implementation-detail placeholder (std::__*, type-parameter, auto-parameter). + if !display_name.contains("::") + && canonical_name.contains("::") + && !canonical_name.starts_with("std::__") + && !canonical_name.contains("type-parameter-") + && !canonical_name.contains("auto-parameter-") + { + canonical_name + } else { + display_name + } +} + +fn is_alias_type(ty: &Type) -> bool { + matches!( + ty.get_declaration() + .map(|declaration| declaration.get_kind()), + Some(EntityKind::TypedefDecl | EntityKind::TypeAliasDecl) + ) +} + +fn resolve_decl_based(ty: &Type) -> Option { + // Declaration-derived id is the primary identity source for user-defined types. + // Template arguments are recursively resolved into the same semantic model. + let declaration = ty.get_declaration()?; + let base = entity_id_from_decl(&declaration); + let args = ty + .get_template_argument_types() + .unwrap_or_default() + .into_iter() + .flatten() + .map(|argument| resolve_type(&argument)) + .collect::>(); + + (!args.is_empty()) + .then_some(ResolvedType::Template { + base: base.clone(), + args, + }) + .or(Some(ResolvedType::UserDefined(base))) +} + +fn unknown(ty: &Type) -> ResolvedType { + ResolvedType::Unknown(ty.get_display_name()) +} + +fn entity_id_from_decl(entity: &Entity) -> String { + if entity.get_kind() == EntityKind::TemplateTemplateParameter { + return entity.get_name().unwrap_or_default(); + } + build_fqn_from_entity(entity) + .trim_start_matches("::") + .to_string() +} + +/// Collapses implementation-detail namespaces such as `std::__1`. +fn collapse_std_internal_namespaces(parts: Vec<(String, bool)>) -> Vec { + let mut collapsed = Vec::with_capacity(parts.len()); + for (name, is_namespace) in parts { + let previous = collapsed.last().map(String::as_str); + let is_std_internal = + is_namespace && previous == Some("std") && is_std_internal_namespace_segment(&name); + if !is_std_internal { + collapsed.push(name); + } + } + collapsed +} + +fn is_std_internal_namespace_segment(name: &str) -> bool { + name.strip_prefix("__") + .map(|rest| !rest.is_empty() && rest.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')) + .unwrap_or(false) +} + +/// Walk semantic parents of an entity to produce `Namespace::Class::Name`. +fn build_fqn_from_entity(entity: &Entity) -> String { + // Traversal is semantic (not lexical) so aliases/nested constructs resolve to + // stable ownership hierarchy used by relationship and id matching. + let mut parts = Vec::new(); + let mut current = Some(*entity); + + while let Some(entity) = current { + match entity.get_kind() { + EntityKind::Namespace => { + if let Some(name) = entity.get_name() { + parts.push((name, true)); + } + } + EntityKind::ClassTemplatePartialSpecialization => { + if let Some(name) = entity.get_display_name().or_else(|| entity.get_name()) { + parts.push((name, false)); + } + } + EntityKind::ClassDecl + | EntityKind::StructDecl + | EntityKind::UnionDecl + | EntityKind::EnumDecl + | EntityKind::ClassTemplate + | EntityKind::TemplateTemplateParameter + | EntityKind::TypedefDecl + | EntityKind::TypeAliasDecl => { + if let Some(name) = entity.get_name() { + parts.push((name, false)); + } + } + _ => break, + } + current = entity.get_semantic_parent(); + } + parts.reverse(); + collapse_std_internal_namespaces(parts).join("::") +} + +#[cfg(test)] +mod tests { + use super::collapse_std_internal_namespaces; + + #[test] + fn collapses_std_internal_namespaces_only_under_std() { + let parts = vec![ + ("std".to_string(), true), + ("__1".to_string(), true), + ("vector".to_string(), false), + ]; + assert_eq!( + collapse_std_internal_namespaces(parts), + vec!["std".to_string(), "vector".to_string()] + ); + } + + #[test] + fn preserves_non_std_internal_namespaces() { + let parts = vec![ + ("foo".to_string(), true), + ("__detail".to_string(), true), + ("Bar".to_string(), false), + ]; + assert_eq!( + collapse_std_internal_namespaces(parts), + vec!["foo".to_string(), "__detail".to_string(), "Bar".to_string()] + ); + } +} diff --git a/cpp/libclang/src/visitor/src/visitor.rs b/cpp/libclang/src/visitor/src/visitor.rs index 0b3d45bf..5fcd6410 100644 --- a/cpp/libclang/src/visitor/src/visitor.rs +++ b/cpp/libclang/src/visitor/src/visitor.rs @@ -11,38 +11,17 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use clang::{Entity, EntityKind}; + +use crate::clang_adapter::scope::namespace_id; +use crate::clang_adapter::source_filter; use crate::class_visitor::ClassVisitor; use crate::context::VisitContext; use crate::enum_visitor::EnumVisitor; use crate::function_visitor::FunctionVisitor; -use crate::source_filter; -use clang::{Entity, EntityKind}; pub trait AstVisitor { fn visit(ctx: &mut VisitContext, entity: Entity); - - fn get_namespace_id(entity: &Entity) -> Option { - namespace_id(entity) - } -} - -fn namespace_id(entity: &Entity) -> Option { - let mut stack: Vec = vec![]; - let mut current = entity.get_semantic_parent(); - while let Some(parent) = current { - if parent.get_kind() == EntityKind::Namespace { - if let Some(name) = parent.get_name() { - stack.push(name); - } - } - current = parent.get_semantic_parent(); - } - - if stack.is_empty() { - None - } else { - Some(stack.into_iter().rev().collect::>().join("::")) - } } pub struct Visitor<'a> { @@ -60,7 +39,6 @@ impl<'a> Visitor<'a> { } fn visit_recursive(&mut self, entity: Entity) { - self.ctx.is_templated = false; if is_ignored_entity(entity) { return; } @@ -70,7 +48,6 @@ impl<'a> Visitor<'a> { ClassVisitor::visit(self.ctx, entity); } EntityKind::ClassTemplate | EntityKind::ClassTemplatePartialSpecialization => { - self.ctx.is_templated = true; ClassVisitor::visit(self.ctx, entity); // ClassTemplate parsing already processes all members, // so skip generic child recursion to avoid double-processing. From 42c11c2f6305ae709c9b5a092a1a41bbd067b74b Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Mon, 17 Aug 2026 18:08:25 +0800 Subject: [PATCH 2/2] fix(libclang): resolve relationships to nested types --- .../cases/nested_type_relationship/BUILD | 25 ++++++ .../nested_type_relationship/expected.json | 76 +++++++++++++++++++ .../nested_type_relationship/run_test.rs | 19 +++++ .../nested_type_relationship/transport.cpp | 26 +++++++ .../src/visitor/src/clang_adapter/scope.rs | 30 ++++++++ cpp/libclang/src/visitor/src/class_visitor.rs | 8 +- cpp/libclang/src/visitor/src/enum_visitor.rs | 7 +- 7 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 cpp/libclang/integration_test/cases/nested_type_relationship/BUILD create mode 100644 cpp/libclang/integration_test/cases/nested_type_relationship/expected.json create mode 100644 cpp/libclang/integration_test/cases/nested_type_relationship/run_test.rs create mode 100644 cpp/libclang/integration_test/cases/nested_type_relationship/transport.cpp diff --git a/cpp/libclang/integration_test/cases/nested_type_relationship/BUILD b/cpp/libclang/integration_test/cases/nested_type_relationship/BUILD new file mode 100644 index 00000000..597818b8 --- /dev/null +++ b/cpp/libclang/integration_test/cases/nested_type_relationship/BUILD @@ -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 +# ******************************************************************************* +load("//cpp/libclang/integration_test:test_rules.bzl", "cpp_parser_integration_test") + +cc_library( + name = "nested_type_relationship", + srcs = ["transport.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_nested_type_relationship", + expected_output = ["expected.json"], + target = ":nested_type_relationship", +) diff --git a/cpp/libclang/integration_test/cases/nested_type_relationship/expected.json b/cpp/libclang/integration_test/cases/nested_type_relationship/expected.json new file mode 100644 index 00000000..ab7108fd --- /dev/null +++ b/cpp/libclang/integration_test/cases/nested_type_relationship/expected.json @@ -0,0 +1,76 @@ +{ + "types": { + "demo::Consumer": { + "id": "demo::Consumer", + "name": "Consumer", + "enclosing_namespace_id": "demo", + "entity_type": "Class", + "enum_literals": [], + "methods": [], + "relationships": [ + { + "source": "demo::Consumer", + "target": "demo::Outer::Inner", + "relation_type": "Composition", + "source_multiplicity": null, + "target_multiplicity": null, + "source_location": { + "file": "", + "line": 23 + } + } + ], + "template_parameters": null, + "type_aliases": [], + "variables": [ + { + "name": "inner_", + "data_type": "demo::Outer::Inner", + "is_static": false, + "visibility": "private", + "source_location": { + "file": "", + "line": 23 + } + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/cases/nested_type_relationship/transport.cpp", + "line": 21 + } + }, + "demo::Outer": { + "id": "demo::Outer", + "name": "Outer", + "enclosing_namespace_id": "demo", + "entity_type": "Class", + "enum_literals": [], + "methods": [], + "relationships": [], + "template_parameters": null, + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/nested_type_relationship/transport.cpp", + "line": 16 + } + }, + "demo::Outer::Inner": { + "id": "demo::Outer::Inner", + "name": "Inner", + "enclosing_namespace_id": "demo", + "entity_type": "Class", + "enum_literals": [], + "methods": [], + "relationships": [], + "template_parameters": null, + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/nested_type_relationship/transport.cpp", + "line": 18 + } + } + }, + "functions": [] +} diff --git a/cpp/libclang/integration_test/cases/nested_type_relationship/run_test.rs b/cpp/libclang/integration_test/cases/nested_type_relationship/run_test.rs new file mode 100644 index 00000000..d99aaf6e --- /dev/null +++ b/cpp/libclang/integration_test/cases/nested_type_relationship/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_nested_type_relationship() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/cases/nested_type_relationship/transport.cpp b/cpp/libclang/integration_test/cases/nested_type_relationship/transport.cpp new file mode 100644 index 00000000..360ee992 --- /dev/null +++ b/cpp/libclang/integration_test/cases/nested_type_relationship/transport.cpp @@ -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 + ********************************************************************************/ + +namespace demo { + +class Outer { + public: + class Inner {}; +}; + +class Consumer { + private: + Outer::Inner inner_; +}; + +} // namespace demo diff --git a/cpp/libclang/src/visitor/src/clang_adapter/scope.rs b/cpp/libclang/src/visitor/src/clang_adapter/scope.rs index 1b0ccb0e..b9c61e7b 100644 --- a/cpp/libclang/src/visitor/src/clang_adapter/scope.rs +++ b/cpp/libclang/src/visitor/src/clang_adapter/scope.rs @@ -42,3 +42,33 @@ pub(crate) fn namespace_id(entity: &Entity) -> Option { (!path.is_empty()).then(|| path.join("::")) } +/// Returns named semantic parents that can own a nested C++ declaration. +/// +/// This intentionally excludes aliases, template parameters, and enums: they +/// appear in libclang's semantic-parent chain but cannot own named nested types. +pub(crate) fn semantic_parent_id(entity: &Entity) -> Option { + let mut parents = Vec::new(); + let mut current = entity.get_semantic_parent(); + + while let Some(parent) = current { + let name = match parent.get_kind() { + EntityKind::Namespace + | EntityKind::ClassDecl + | EntityKind::StructDecl + | EntityKind::UnionDecl + | EntityKind::ClassTemplate => parent.get_name(), + EntityKind::ClassTemplatePartialSpecialization => { + parent.get_display_name().or_else(|| parent.get_name()) + } + _ => None, + }; + + if let Some(name) = name { + parents.push(name); + } + current = parent.get_semantic_parent(); + } + + parents.reverse(); + (!parents.is_empty()).then(|| parents.join("::")) +} diff --git a/cpp/libclang/src/visitor/src/class_visitor.rs b/cpp/libclang/src/visitor/src/class_visitor.rs index ecf701d6..7373e016 100644 --- a/cpp/libclang/src/visitor/src/class_visitor.rs +++ b/cpp/libclang/src/visitor/src/class_visitor.rs @@ -19,7 +19,7 @@ use class_diagram::{ }; use cpp_semantics::ResolvedType; -use crate::clang_adapter::scope::namespace_id; +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, @@ -39,9 +39,10 @@ impl AstVisitor for ClassVisitor { }; let namespace = namespace_id(&entity); + let semantic_parent = semantic_parent_id(&entity); if let Some((builder, mut class_entity)) = - Self::visit_class(&entity, namespace.as_deref()) + Self::visit_class(&entity, semantic_parent.as_deref(), namespace.as_deref()) { class_entity.template_parameters = template_params; ctx.parsed_class_info.push(builder); @@ -59,6 +60,7 @@ impl ClassVisitor { fn visit_class( entity: &Entity, + semantic_parent: Option<&str>, namespace: Option<&str>, ) -> Option<(ParsedClassInfo, SimpleEntity)> { let Some(name) = entity.get_name() else { @@ -66,7 +68,7 @@ impl ClassVisitor { return None; }; - let id = class_entity_id(entity, namespace, &name); + let id = class_entity_id(entity, semantic_parent, &name); let mut builder = ParsedClassInfo { id: id.clone(), diff --git a/cpp/libclang/src/visitor/src/enum_visitor.rs b/cpp/libclang/src/visitor/src/enum_visitor.rs index b8ebc51c..cf938245 100644 --- a/cpp/libclang/src/visitor/src/enum_visitor.rs +++ b/cpp/libclang/src/visitor/src/enum_visitor.rs @@ -14,7 +14,7 @@ use clang::Entity; use class_diagram::{EntityType, EnumLiteral, SimpleEntity}; -use crate::clang_adapter::scope::namespace_id; +use crate::clang_adapter::scope::{namespace_id, semantic_parent_id}; use crate::clang_adapter::source_location::parse_source_location; use crate::context::VisitContext; use crate::visitor::AstVisitor; @@ -36,8 +36,9 @@ impl EnumVisitor { return None; }; let namespace_id = namespace_id(&entity); - let full_qualified_id = if let Some(namespace_id) = &namespace_id { - format!("{}::{}", namespace_id, name) + let semantic_parent = semantic_parent_id(&entity); + let full_qualified_id = if let Some(semantic_parent) = &semantic_parent { + format!("{}::{}", semantic_parent, name) } else { name.clone() };