diff --git a/cpp/libclang/integration_test/cases/dependent_decltype_base/BUILD b/cpp/libclang/integration_test/cases/dependent_decltype_base/BUILD new file mode 100644 index 00000000..12c64c0f --- /dev/null +++ b/cpp/libclang/integration_test/cases/dependent_decltype_base/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 = "dependent_decltype_base", + srcs = ["dependent_base.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_dependent_decltype_base", + expected_output = ["expected.json"], + target = ":dependent_decltype_base", +) diff --git a/cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp b/cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp new file mode 100644 index 00000000..a6fd4a82 --- /dev/null +++ b/cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp @@ -0,0 +1,45 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +class ContainerBase { +public: + virtual ~ContainerBase() = default; +}; + +template T declval(); + +template +auto is_maplike_container_impl(T value) -> decltype(value.begin()); + +// Regression test for a real parser crash: a base class specifier whose type +// depends on an uninstantiated template parameter, e.g. through the common +// decltype/SFINAE detection idiom, cannot be resolved to a concrete entity id +// without template instantiation. The parser must not panic on this; it +// should simply skip the unresolvable inheritance relationship. +template +struct IsMaplikeContainer : decltype(is_maplike_container_impl(declval())){}; + +// A normal, resolvable base class must still be parsed correctly even though +// the translation unit also contains the unresolvable template above. +class Container : public ContainerBase {}; + +// A concrete, non-template return type used purely so `decltype(make_widget())` +// below has something real to resolve to. +class Widget { +public: + Widget(); +}; + +Widget make_widget(); + +class ConcreteWidgetUser : public decltype(make_widget()){}; diff --git a/cpp/libclang/integration_test/cases/dependent_decltype_base/expected.json b/cpp/libclang/integration_test/cases/dependent_decltype_base/expected.json new file mode 100644 index 00000000..c5d06501 --- /dev/null +++ b/cpp/libclang/integration_test/cases/dependent_decltype_base/expected.json @@ -0,0 +1,147 @@ +{ + "types": { + "ContainerBase": { + "id": "ContainerBase", + "name": "ContainerBase", + "enclosing_namespace_id": null, + "entity_type": "Class", + "enum_literals": [], + "methods": [ + { + "name": "~ContainerBase", + "return_type": null, + "parameters": [], + "modifiers": [ + "Virtual", + "Destructor" + ], + "template_parameters": null, + "visibility": "public", + "source_location": { + "file": "", + "line": 16 + } + } + ], + "relationships": [], + "template_parameters": null, + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 14 + } + }, + "IsMaplikeContainer": { + "id": "IsMaplikeContainer", + "name": "IsMaplikeContainer", + "enclosing_namespace_id": null, + "entity_type": "Class", + "enum_literals": [], + "methods": [], + "relationships": [], + "template_parameters": [ + { + "Type": { + "name": "T", + "is_pack": false + } + } + ], + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 30 + } + }, + "Container": { + "id": "Container", + "name": "Container", + "enclosing_namespace_id": null, + "entity_type": "Class", + "enum_literals": [], + "methods": [], + "relationships": [ + { + "source": "Container", + "target": "ContainerBase", + "relation_type": "Inheritance", + "source_multiplicity": null, + "target_multiplicity": null, + "source_location": { + "file": "", + "line": 34 + } + } + ], + "template_parameters": null, + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 34 + } + }, + "Widget": { + "id": "Widget", + "name": "Widget", + "enclosing_namespace_id": null, + "entity_type": "Class", + "enum_literals": [], + "methods": [ + { + "name": "Widget", + "return_type": null, + "parameters": [], + "modifiers": [ + "Constructor" + ], + "template_parameters": null, + "visibility": "public", + "source_location": { + "file": "", + "line": 40 + } + } + ], + "relationships": [], + "template_parameters": null, + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 38 + } + }, + "ConcreteWidgetUser": { + "id": "ConcreteWidgetUser", + "name": "ConcreteWidgetUser", + "enclosing_namespace_id": null, + "entity_type": "Class", + "enum_literals": [], + "methods": [], + "relationships": [ + { + "source": "ConcreteWidgetUser", + "target": "Widget", + "relation_type": "Inheritance", + "source_multiplicity": null, + "target_multiplicity": null, + "source_location": { + "file": "", + "line": 45 + } + } + ], + "template_parameters": null, + "type_aliases": [], + "variables": [], + "source_location": { + "file": "cpp/libclang/integration_test/cases/dependent_decltype_base/dependent_base.cpp", + "line": 45 + } + } + }, + "functions": [] +} diff --git a/cpp/libclang/integration_test/cases/dependent_decltype_base/run_test.rs b/cpp/libclang/integration_test/cases/dependent_decltype_base/run_test.rs new file mode 100644 index 00000000..28706d87 --- /dev/null +++ b/cpp/libclang/integration_test/cases/dependent_decltype_base/run_test.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 +// ******************************************************************************* + +use test_framework::run_parser_case; +#[test] +fn test_dependent_decltype_base() { + run_parser_case(); +} diff --git a/cpp/libclang/src/visitor/src/class_parser_helper.rs b/cpp/libclang/src/visitor/src/class_parser_helper.rs index db0e980b..37c18ac9 100644 --- a/cpp/libclang/src/visitor/src/class_parser_helper.rs +++ b/cpp/libclang/src/visitor/src/class_parser_helper.rs @@ -48,6 +48,14 @@ pub enum ResolvedType { }, 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 { @@ -96,7 +104,9 @@ impl ResolvedType { /// - Wrapper/qualifier/array nodes delegate to their inner element. pub fn relationship_target_entity_id(&self) -> Option<&str> { match self { - ResolvedType::Builtin(_) | ResolvedType::Unknown(_) => None, + ResolvedType::Builtin(_) | ResolvedType::Unknown(_) | ResolvedType::Dependent(_) => { + None + } ResolvedType::UserDefined(id) => Some(id), ResolvedType::Template { base, args } => args .iter() @@ -252,6 +262,8 @@ impl ResolvedType { }, ResolvedType::Unknown(s) => s.clone(), + + ResolvedType::Dependent(s) => s.clone(), } } @@ -332,7 +344,10 @@ fn contains_template_type(resolved: &ResolvedType) -> bool { | ResolvedType::Const(inner) | ResolvedType::Volatile(inner) => contains_template_type(inner), ResolvedType::Array { element, .. } => contains_template_type(element), - ResolvedType::Builtin(_) | ResolvedType::UserDefined(_) | ResolvedType::Unknown(_) => false, + ResolvedType::Builtin(_) + | ResolvedType::UserDefined(_) + | ResolvedType::Unknown(_) + | ResolvedType::Dependent(_) => false, } } @@ -520,7 +535,9 @@ fn resolve_named_type(original: &Type, canonical: &Type) -> ResolvedType { // Fallback order matters: // 1) source declaration (preserves local spelling when available) // 2) canonical declaration (captures normalized identity) - // 3) unknown name heuristic + // 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; } @@ -529,9 +546,24 @@ fn resolve_named_type(original: &Type, canonical: &Type) -> ResolvedType { return resolved; } + if is_dependent_expression_type(original) { + return ResolvedType::Dependent(resolve_unknown_name(original, canonical)); + } + ResolvedType::Unknown(resolve_unknown_name(original, canonical)) } +/// 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(); 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 2af3ae9a..728d0793 100644 --- a/cpp/libclang/src/visitor/src/class_parser_helper_test.rs +++ b/cpp/libclang/src/visitor/src/class_parser_helper_test.rs @@ -195,3 +195,55 @@ fn collapse_std_internal_namespaces_does_not_drop_non_std_internal_namespaces() 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_visitor.rs b/cpp/libclang/src/visitor/src/class_visitor.rs index 3c939850..cb4d010b 100644 --- a/cpp/libclang/src/visitor/src/class_visitor.rs +++ b/cpp/libclang/src/visitor/src/class_visitor.rs @@ -406,16 +406,15 @@ fn infer_entity_type_from_members(kind: EntityKind, class: &SimpleEntity) -> Ent // Relationship part fn build_relationships_for_class(ctx: &mut VisitContext, builder: &ParsedClassInfo) { for base in &builder.base_classes { - let resolved_base = base - .resolved_type - .referenced_entity_id() - .unwrap_or_else(|| { - panic!( - "Unresolved base type '{}' referenced by '{}'", - base.resolved_type.render_for_display(), - builder.id - ) - }); + let Some(resolved_base) = base.resolved_type.referenced_entity_id() else { + eprintln!( + "Warning: unable to resolve base type '{}' for '{}'; \ + skipping inheritance relationship (dependent/decltype expression?)", + 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 diff --git a/cpp/libclang/src/visitor/src/class_visitor_test.rs b/cpp/libclang/src/visitor/src/class_visitor_test.rs index 949d7739..b26f6b48 100644 --- a/cpp/libclang/src/visitor/src/class_visitor_test.rs +++ b/cpp/libclang/src/visitor/src/class_visitor_test.rs @@ -12,7 +12,9 @@ //////////////////////////////////////////////////////////////////////////////////// use class_diagram::{RelationType, SimpleEntity, SourceLocation}; -use visit_tu::context::{ParsedClassInfo, ParsedMethodType, ParsedVariableType, VisitContext}; +use visit_tu::context::{ + ParsedBaseClass, ParsedClassInfo, ParsedMethodType, ParsedVariableType, VisitContext, +}; use visit_tu::{ClassVisitor, ResolvedType}; #[test] @@ -93,3 +95,89 @@ fn resolve_relationships_uses_variable_and_method_source_locations() { 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) + ); +}