diff --git a/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/BUILD b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/BUILD new file mode 100644 index 00000000..feca45b2 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/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 = "std_internal_namespace_rendering", + srcs = ["transport.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_std_internal_namespace_rendering", + expected_output = ["expected.json"], + target = ":std_internal_namespace_rendering", +) diff --git a/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/expected.json b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/expected.json new file mode 100644 index 00000000..0c85a670 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/expected.json @@ -0,0 +1,61 @@ +{ + "functions": [], + "types": { + "TextCache": { + "enclosing_namespace_id": null, + "entity_type": "Class", + "enum_literals": [], + "id": "TextCache", + "methods": [ + { + "modifiers": [], + "name": "Find", + "parameters": [ + { + "is_variadic": false, + "name": "key", + "param_type": "const std::string &" + } + ], + "return_type": "std::string_view", + "source_location": { + "file": "cpp/libclang/integration_test/cases/std_internal_namespace_rendering/transport.cpp", + "line": 20 + }, + "template_parameters": null, + "visibility": "public" + } + ], + "name": "TextCache", + "relationships": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/std_internal_namespace_rendering/transport.cpp", + "line": 17 + }, + "template_parameters": null, + "type_aliases": [], + "variables": [ + { + "data_type": "std::string", + "is_static": false, + "name": "name_", + "source_location": { + "file": "cpp/libclang/integration_test/cases/std_internal_namespace_rendering/transport.cpp", + "line": 23 + }, + "visibility": "private" + }, + { + "data_type": "std::unordered_map", + "is_static": false, + "name": "cache_", + "source_location": { + "file": "cpp/libclang/integration_test/cases/std_internal_namespace_rendering/transport.cpp", + "line": 24 + }, + "visibility": "private" + } + ] + } + } +} diff --git a/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/run_test.rs b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/run_test.rs new file mode 100644 index 00000000..ddaeff76 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/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_std_internal_namespace_rendering() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/transport.cpp b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/transport.cpp new file mode 100644 index 00000000..4fa3d6c0 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_internal_namespace_rendering/transport.cpp @@ -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 + ********************************************************************************/ +#include +#include +#include + +class TextCache +{ + public: + std::string_view Find(const std::string& key); + + private: + std::string name_; + std::unordered_map cache_; +}; diff --git a/cpp/libclang/integration_test/cases/std_template_specialization_filter/BUILD b/cpp/libclang/integration_test/cases/std_template_specialization_filter/BUILD new file mode 100644 index 00000000..2aad9a11 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_template_specialization_filter/BUILD @@ -0,0 +1,26 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("//cpp/libclang/integration_test:test_rules.bzl", "cpp_parser_integration_test") + +cc_library( + name = "std_template_specialization_filter", + srcs = ["specialization.cpp"], + hdrs = ["specialization.h"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_std_template_specialization_filter", + expected_output = ["expected.json"], + target = ":std_template_specialization_filter", +) diff --git a/cpp/libclang/integration_test/cases/std_template_specialization_filter/expected.json b/cpp/libclang/integration_test/cases/std_template_specialization_filter/expected.json new file mode 100644 index 00000000..069bf2d8 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_template_specialization_filter/expected.json @@ -0,0 +1,32 @@ +{ + "functions": [], + "types": { + "demo::Key": { + "enclosing_namespace_id": "demo", + "entity_type": "Struct", + "enum_literals": [], + "id": "demo::Key", + "methods": [], + "name": "Key", + "relationships": [], + "source_location": { + "file": "./cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.h", + "line": 23 + }, + "template_parameters": null, + "type_aliases": [], + "variables": [ + { + "data_type": "std::uint32_t", + "is_static": false, + "name": "value", + "source_location": { + "file": "./cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.h", + "line": 25 + }, + "visibility": "public" + } + ] + } + } +} diff --git a/cpp/libclang/integration_test/cases/std_template_specialization_filter/run_test.rs b/cpp/libclang/integration_test/cases/std_template_specialization_filter/run_test.rs new file mode 100644 index 00000000..297a7ec3 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_template_specialization_filter/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_std_template_specialization_filter() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.cpp b/cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.cpp new file mode 100644 index 00000000..c3b4ff90 --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.cpp @@ -0,0 +1,13 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.h" diff --git a/cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.h b/cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.h new file mode 100644 index 00000000..80ad44ff --- /dev/null +++ b/cpp/libclang/integration_test/cases/std_template_specialization_filter/specialization.h @@ -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 + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef CPP_LIBCLANG_INTEGRATION_TEST_CASES_STD_TEMPLATE_SPECIALIZATION_FILTER_SPECIALIZATION_H +#define CPP_LIBCLANG_INTEGRATION_TEST_CASES_STD_TEMPLATE_SPECIALIZATION_FILTER_SPECIALIZATION_H + +#include +#include +#include + +namespace demo +{ + +struct Key +{ + std::uint32_t value; +}; + +} // namespace demo + +namespace std +{ + +template <> +struct hash +{ + std::size_t operator()(const demo::Key& key) const noexcept + { + return std::hash{}(key.value); + } +}; + +} // namespace std + +#endif diff --git a/cpp/libclang/src/main.rs b/cpp/libclang/src/main.rs index 82bfb3c0..9bf68df2 100644 --- a/cpp/libclang/src/main.rs +++ b/cpp/libclang/src/main.rs @@ -22,8 +22,7 @@ use class_diagram::{ClassDiagram, SimpleEntity}; use class_serializer::ClassSerializer; use utils::{render_entity_tree, write_debug_json, write_entity_tree, write_fbs_output}; -use visit_tu::visitor; -use visit_tu::{FunctionDef, VisitContext, Visitor}; +use visit_tu::{is_external_dependency_path, FunctionDef, VisitContext, Visitor}; #[derive(ClapParser, Debug)] #[command(name = "cpp_parser")] @@ -120,7 +119,7 @@ fn parse_file( debug!("Parsing TU: {:?}", file); if let Some(path_str) = file.to_str() { - if visitor::is_external_dependency_path(path_str) { + if is_external_dependency_path(path_str) { debug!("Skipping external dependency file: {:?}", file); return; } diff --git a/cpp/libclang/src/visitor/BUILD b/cpp/libclang/src/visitor/BUILD index 06724c58..452498cf 100644 --- a/cpp/libclang/src/visitor/BUILD +++ b/cpp/libclang/src/visitor/BUILD @@ -21,6 +21,7 @@ rust_library( "src/enum_visitor.rs", "src/function_visitor.rs", "src/lib.rs", + "src/source_filter.rs", "src/visitor.rs", ], visibility = ["//cpp/libclang:__subpackages__"], @@ -38,6 +39,7 @@ rust_test( srcs = [ "src/class_parser_helper.rs", "src/class_parser_helper_test.rs", + "src/source_filter.rs", ], deps = [ "@crates//:clang", diff --git a/cpp/libclang/src/visitor/src/class_parser_helper.rs b/cpp/libclang/src/visitor/src/class_parser_helper.rs index 9974ee05..ee5032e1 100644 --- a/cpp/libclang/src/visitor/src/class_parser_helper.rs +++ b/cpp/libclang/src/visitor/src/class_parser_helper.rs @@ -13,6 +13,7 @@ #![cfg_attr(test, allow(dead_code))] +use crate::source_filter; use clang::{Entity, EntityKind, Type, TypeKind}; use serde::{Deserialize, Serialize}; @@ -306,26 +307,11 @@ fn is_declared_in_external_or_system_header(ty: &Type) -> bool { .and_then(|decl| decl.get_location()) .map(|location| { let (path, _, _) = location.get_presumed_location(); - is_external_or_system_path(&path) + source_filter::is_external_or_system_path(&path) }) .unwrap_or(false) } -fn is_external_or_system_path(path: &str) -> bool { - is_system_header_path(path) - || path.contains("/external/") - || path.contains("external/") - || path.contains("_virtual_includes/") - || (path.contains("bazel-out/") && path.contains("/external/")) -} - -fn is_system_header_path(path: &str) -> bool { - path.starts_with("/usr/include") - || path.starts_with("/usr/local/include") - || path.starts_with("/opt/") - || path.contains("/gcc/") -} - fn contains_template_type(resolved: &ResolvedType) -> bool { match resolved { ResolvedType::Template { .. } => true, diff --git a/cpp/libclang/src/visitor/src/class_parser_helper_test.rs b/cpp/libclang/src/visitor/src/class_parser_helper_test.rs index a2b70fb6..af8ebc19 100644 --- a/cpp/libclang/src/visitor/src/class_parser_helper_test.rs +++ b/cpp/libclang/src/visitor/src/class_parser_helper_test.rs @@ -13,6 +13,8 @@ #[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}; diff --git a/cpp/libclang/src/visitor/src/lib.rs b/cpp/libclang/src/visitor/src/lib.rs index 382b767a..ab762ad9 100644 --- a/cpp/libclang/src/visitor/src/lib.rs +++ b/cpp/libclang/src/visitor/src/lib.rs @@ -16,6 +16,7 @@ mod class_visitor; pub mod context; mod enum_visitor; mod function_visitor; +mod source_filter; pub mod visitor; pub use class_parser_helper::ResolvedType; @@ -24,5 +25,6 @@ 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/source_filter.rs b/cpp/libclang/src/visitor/src/source_filter.rs new file mode 100644 index 00000000..6738f561 --- /dev/null +++ b/cpp/libclang/src/visitor/src/source_filter.rs @@ -0,0 +1,111 @@ +// ******************************************************************************* +// 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 +// ******************************************************************************* + +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/"]; +const EXCLUDED_TOP_LEVEL_NAMESPACES: &[&str] = &["std", "__gnu_cxx"]; + +/// Returns whether a path belongs to a system header location. +pub(crate) fn is_system_header_path(path: &str) -> bool { + SYSTEM_HEADER_PREFIXES + .iter() + .any(|prefix| path.starts_with(prefix)) + || SYSTEM_HEADER_SUBSTRINGS + .iter() + .any(|fragment| path.contains(fragment)) +} + +/// Returns whether a path belongs to a Bazel external dependency. +pub fn is_external_dependency_path(path: &str) -> bool { + EXTERNAL_DEP_PATH_SUBSTRINGS + .iter() + .any(|fragment| path.contains(fragment)) + || (path.contains("bazel-out/") && path.contains("/external/")) +} + +/// Returns whether a path belongs to a header that is outside the parsed model. +pub(crate) fn is_external_or_system_path(path: &str) -> bool { + is_system_header_path(path) || is_external_dependency_path(path) +} + +/// 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 { + let Some(namespace) = namespace else { + return false; + }; + + let top_level_namespace = namespace.split("::").next().unwrap_or(namespace); + EXCLUDED_TOP_LEVEL_NAMESPACES.contains(&top_level_namespace) +} + +#[cfg(test)] +mod tests { + use super::{ + is_excluded_namespace, is_external_dependency_path, is_external_or_system_path, + is_system_header_path, + }; + + #[test] + fn classifies_system_header_paths() { + for path in [ + "/usr/include/c++/v1/vector", + "/usr/local/include/library/header.hpp", + "/opt/sdk/include/api.hpp", + "/toolchains/gcc/include/c++/vector", + ] { + assert!(is_system_header_path(path), "expected system path: {path}"); + assert!(is_external_or_system_path(path)); + } + } + + #[test] + fn classifies_bazel_external_dependency_paths() { + for path in [ + "/workspace/external/flatbuffers/include/flatbuffers.h", + "external/flatbuffers/include/flatbuffers.h", + "/workspace/bazel-out/k8-fastbuild/bin/_virtual_includes/runtime/flatbuffers.h", + ] { + assert!( + is_external_dependency_path(path), + "expected external dependency path: {path}" + ); + assert!(is_external_or_system_path(path)); + } + } + + #[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)); + } + + #[test] + fn excludes_standard_library_namespaces_even_for_workspace_sources() { + assert!(is_excluded_namespace(Some("std"))); + assert!(is_excluded_namespace(Some("std::__1"))); + assert!(is_excluded_namespace(Some("__gnu_cxx"))); + assert!(is_excluded_namespace(Some("__gnu_cxx::__detail"))); + } + + #[test] + fn keeps_project_namespaces_in_the_model() { + assert!(!is_excluded_namespace(None)); + assert!(!is_excluded_namespace(Some("score::mw::com::impl"))); + assert!(!is_excluded_namespace(Some("amsr"))); + } +} diff --git a/cpp/libclang/src/visitor/src/visitor.rs b/cpp/libclang/src/visitor/src/visitor.rs index 3734a680..be7aa127 100644 --- a/cpp/libclang/src/visitor/src/visitor.rs +++ b/cpp/libclang/src/visitor/src/visitor.rs @@ -15,37 +15,33 @@ 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}; -// Note: Temporary workaround: use file paths to exclude external dependencies. -// This path-based filtering is not ideal, but should work in most cases. -// As a follow-up, source file paths can be collected into a JSON file. -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/"]; - pub trait AstVisitor { fn visit(ctx: &mut VisitContext, entity: Entity); fn get_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); - } + 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(); } + current = parent.get_semantic_parent(); + } - if stack.is_empty() { - None - } else { - Some(stack.into_iter().rev().collect::>().join("::")) - } + if stack.is_empty() { + None + } else { + Some(stack.into_iter().rev().collect::>().join("::")) } } @@ -94,24 +90,9 @@ impl<'a> Visitor<'a> { fn is_ignored_entity(entity: Entity) -> bool { if let Some(location) = entity.get_location() { let (file, _line, _column) = location.get_presumed_location(); - is_system_header_path(&file) || is_external_dependency_path(&file) + source_filter::is_external_or_system_path(&file) + || source_filter::is_excluded_namespace(namespace_id(&entity).as_deref()) } else { false } } - -fn is_system_header_path(path: &str) -> bool { - SYSTEM_HEADER_PREFIXES - .iter() - .any(|prefix| path.starts_with(prefix)) - || SYSTEM_HEADER_SUBSTRINGS - .iter() - .any(|fragment| path.contains(fragment)) -} - -pub fn is_external_dependency_path(path: &str) -> bool { - EXTERNAL_DEP_PATH_SUBSTRINGS - .iter() - .any(|fragment| path.contains(fragment)) - || (path.contains("bazel-out/") && path.contains("/external/")) -}