From 06d62db075f3b3c27f747a672753d1c43244f11d Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Tue, 1 Sep 2026 16:30:56 +0800 Subject: [PATCH 1/2] Refactor function extraction -- If branch --- MODULE.bazel | 1 + cpp/libclang/src/semantics/BUILD | 7 +- cpp/libclang/src/semantics/src/callable.rs | 90 +++ .../src/semantics/src/control_flow.rs | 80 +++ cpp/libclang/src/semantics/src/lib.rs | 4 + .../src/visitor/src/clang_adapter/scope.rs | 74 ++- .../src/clang_adapter/source_location.rs | 8 + cpp/libclang/src/visitor/src/context.rs | 3 +- .../src/visitor/src/function_visitor.rs | 578 ++++++++++++------ cpp/libclang/src/visitor/src/lib.rs | 6 +- cpp/libclang/src/visitor/src/visitor.rs | 45 +- tools/metamodel/sequence/sequence_logic.rs | 26 - 12 files changed, 706 insertions(+), 216 deletions(-) create mode 100644 cpp/libclang/src/semantics/src/callable.rs create mode 100644 cpp/libclang/src/semantics/src/control_flow.rs diff --git a/MODULE.bazel b/MODULE.bazel index f591b381..0718042c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -125,6 +125,7 @@ crate.spec( version = "1.0", ) crate.spec( + features = ["preserve_order"], package = "serde_json", version = "1.0", ) diff --git a/cpp/libclang/src/semantics/BUILD b/cpp/libclang/src/semantics/BUILD index 912f4609..1bf63db2 100644 --- a/cpp/libclang/src/semantics/BUILD +++ b/cpp/libclang/src/semantics/BUILD @@ -16,11 +16,16 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") rust_library( name = "cpp_semantics", srcs = [ + "src/callable.rs", + "src/control_flow.rs", "src/lib.rs", "src/resolved_type.rs", ], visibility = ["//cpp/libclang:__subpackages__"], - deps = ["@crates//:serde"], + deps = [ + "//tools/metamodel/common:source_location", + "@crates//:serde", + ], ) rust_test( diff --git a/cpp/libclang/src/semantics/src/callable.rs b/cpp/libclang/src/semantics/src/callable.rs new file mode 100644 index 00000000..386fa7ff --- /dev/null +++ b/cpp/libclang/src/semantics/src/callable.rs @@ -0,0 +1,90 @@ +// ******************************************************************************* +// 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}; + +use crate::{BodyItem, ResolvedType}; + +/// The semantic scope that declares a C++ callable. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Scope { + /// The C++ global namespace (`::`). + Global, + /// A namespace path, such as `company::network`. + Namespace(Vec), + /// A type, optionally nested in namespaces and other types. + Type { + namespace: Vec, + type_path: Vec, + }, +} + +impl Scope { + /// Returns this scope's C++ qualified name without a leading `::`. + pub fn qualified_name(&self) -> String { + match self { + Self::Global => String::new(), + Self::Namespace(path) => path.join("::"), + Self::Type { + namespace, + type_path, + } => namespace + .iter() + .chain(type_path.iter()) + .cloned() + .collect::>() + .join("::"), + } + } +} + +/// The C++ kind of a callable definition. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum FunctionKind { + Free, + Method, + StaticMethod, + Constructor, + Destructor, + Conversion, +} + +/// Stable semantic identity of a C++ callable. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct FunctionId { + pub scope: Scope, + pub name: String, +} + +impl FunctionId { + /// Returns the function's C++ qualified name without a leading `::`. + pub fn qualified_name(&self) -> String { + let scope_name = self.scope.qualified_name(); + if scope_name.is_empty() { + self.name.clone() + } else { + format!("{scope_name}::{}", self.name) + } + } +} + +/// A function definition extracted from C++ source. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FunctionDef { + pub id: FunctionId, + pub kind: FunctionKind, + /// Constructors and destructors have no ordinary return type. + pub return_type: Option, + /// Body items in execution order. + pub body: Vec, +} diff --git a/cpp/libclang/src/semantics/src/control_flow.rs b/cpp/libclang/src/semantics/src/control_flow.rs new file mode 100644 index 00000000..471cd86d --- /dev/null +++ b/cpp/libclang/src/semantics/src/control_flow.rs @@ -0,0 +1,80 @@ +// ******************************************************************************* +// 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}; +use source_location::SourceLocation; + +/// A single item inside a function, branch, or loop body in execution order. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum BodyItem { + /// A resolved callee's qualified name, without a leading `::`. + Call { + target: String, + source_location: SourceLocation, + }, + Branch { + cases: Vec, + }, + Loop { + kind: LoopKind, + body: Vec, + source_location: SourceLocation, + }, +} + +/// The control-flow form of a loop statement. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LoopKind { + For, + While, + DoWhile, +} + +/// One arm of a conditional branch. `None` represents a final `else` arm. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BranchCase { + /// `None` represents a final `else` arm. + pub guard: Option, + pub body: Vec, + pub source_location: SourceLocation, +} + +/// The condition that controls whether a branch case's body executes. +/// +/// Logical nodes preserve C++ short-circuit structure. Other expressions are +/// retained as opaque source text until they receive dedicated semantic nodes. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum GuardExpression { + Opaque { + text: String, + source_location: SourceLocation, + }, + Call { + /// A resolved callee's qualified name, without a leading `::`. + target: String, + text: String, + source_location: SourceLocation, + }, + Not { + expression: Box, + }, + And { + expressions: Vec, + }, + Or { + expressions: Vec, + }, +} diff --git a/cpp/libclang/src/semantics/src/lib.rs b/cpp/libclang/src/semantics/src/lib.rs index 51418dde..b86ea9c2 100644 --- a/cpp/libclang/src/semantics/src/lib.rs +++ b/cpp/libclang/src/semantics/src/lib.rs @@ -11,6 +11,10 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +mod callable; +mod control_flow; mod resolved_type; +pub use callable::{FunctionDef, FunctionId, FunctionKind, Scope}; +pub use control_flow::{BodyItem, BranchCase, GuardExpression, LoopKind}; pub use resolved_type::{EntityId, ResolvedType}; diff --git a/cpp/libclang/src/visitor/src/clang_adapter/scope.rs b/cpp/libclang/src/visitor/src/clang_adapter/scope.rs index b9c61e7b..4b0877f4 100644 --- a/cpp/libclang/src/visitor/src/clang_adapter/scope.rs +++ b/cpp/libclang/src/visitor/src/clang_adapter/scope.rs @@ -14,6 +14,9 @@ //! Shared semantic-scope extraction helpers for libclang entities. use clang::{Entity, EntityKind}; +use cpp_semantics::Scope; + +// ── Namespace scopes ─────────────────────────────────────────────────────── /// Returns the enclosing namespace names from outermost to innermost. pub(crate) fn namespace_path(entity: &Entity) -> Vec { @@ -42,6 +45,48 @@ pub(crate) fn namespace_id(entity: &Entity) -> Option { (!path.is_empty()).then(|| path.join("::")) } +// ── Type scopes ──────────────────────────────────────────────────────────── + +/// Returns the enclosing type names from outermost to innermost. +pub(crate) fn type_scope_path(entity: &Entity) -> Option> { + let mut types = Vec::new(); + let mut current = Some(*entity); + + while let Some(parent) = current { + if is_type_scope(parent.get_kind()) { + types.push(type_scope_name(&parent)?); + } + current = parent.get_semantic_parent(); + } + + types.reverse(); + (!types.is_empty()).then_some(types) +} + +/// Returns whether an entity kind can own C++ member callables. +pub(crate) fn is_type_scope(kind: EntityKind) -> bool { + matches!( + kind, + EntityKind::ClassDecl + | EntityKind::StructDecl + | EntityKind::UnionDecl + | EntityKind::ClassTemplate + | EntityKind::ClassTemplatePartialSpecialization + ) +} + +fn type_scope_name(entity: &Entity) -> Option { + match entity.get_kind() { + EntityKind::ClassTemplatePartialSpecialization => { + entity.get_display_name().or_else(|| entity.get_name()) + } + kind if is_type_scope(kind) => entity.get_name(), + _ => None, + } +} + +// ── Named declaration parents ────────────────────────────────────────────── + /// Returns named semantic parents that can own a nested C++ declaration. /// /// This intentionally excludes aliases, template parameters, and enums: they @@ -52,14 +97,8 @@ pub(crate) fn semantic_parent_id(entity: &Entity) -> Option { 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()) - } + EntityKind::Namespace => parent.get_name(), + kind if is_type_scope(kind) => type_scope_name(&parent), _ => None, }; @@ -72,3 +111,22 @@ pub(crate) fn semantic_parent_id(entity: &Entity) -> Option { parents.reverse(); (!parents.is_empty()).then(|| parents.join("::")) } + +// ── Callable scopes ──────────────────────────────────────────────────────── + +pub(crate) fn callable_scope(entity: &Entity) -> Option { + match entity.get_semantic_parent() { + Some(parent) if is_type_scope(parent.get_kind()) => Some(Scope::Type { + namespace: namespace_path(&parent), + type_path: type_scope_path(&parent)?, + }), + _ => { + let namespace = namespace_path(entity); + Some(if namespace.is_empty() { + Scope::Global + } else { + Scope::Namespace(namespace) + }) + } + } +} diff --git a/cpp/libclang/src/visitor/src/clang_adapter/source_location.rs b/cpp/libclang/src/visitor/src/clang_adapter/source_location.rs index 9d19cdcf..24e3aec0 100644 --- a/cpp/libclang/src/visitor/src/clang_adapter/source_location.rs +++ b/cpp/libclang/src/visitor/src/clang_adapter/source_location.rs @@ -27,3 +27,11 @@ pub(crate) fn parse_source_location(entity: &Entity) -> SourceLocation { .map(|file| file.get_path().to_string_lossy().to_string()); SourceLocation::new(source_file.unwrap_or_default(), file_location.line) } + +// Returns whether an entity is located in the primary input file of the +// current libclang translation unit, rather than in an included file. +pub(crate) fn is_in_main_file(entity: &Entity) -> bool { + entity + .get_location() + .is_some_and(|location| location.is_in_main_file()) +} diff --git a/cpp/libclang/src/visitor/src/context.rs b/cpp/libclang/src/visitor/src/context.rs index 2e2b1be5..ed00bc77 100644 --- a/cpp/libclang/src/visitor/src/context.rs +++ b/cpp/libclang/src/visitor/src/context.rs @@ -14,8 +14,7 @@ use std::collections::HashMap; use class_diagram::{SimpleEntity, SourceLocation}; -use cpp_semantics::ResolvedType; -use sequence_logic::FunctionDef; +use cpp_semantics::{FunctionDef, ResolvedType}; use serde::{Deserialize, Serialize}; pub type TypeMap = HashMap; diff --git a/cpp/libclang/src/visitor/src/function_visitor.rs b/cpp/libclang/src/visitor/src/function_visitor.rs index 60777e98..8977b477 100644 --- a/cpp/libclang/src/visitor/src/function_visitor.rs +++ b/cpp/libclang/src/visitor/src/function_visitor.rs @@ -11,75 +11,96 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* -//! Visits C++ method entities via libclang and populates [`VisitContext::functions`]. -//! -//! Each method body is represented as an ordered [`Vec`] so that +//! Extracts C++ callable definitions via libclang into [`VisitContext::functions`]. //! calls, branches and loops appear in execution order relative to one another. -use crate::{context::VisitContext, AstVisitor}; use clang::{Entity, EntityKind}; -use sequence_logic::{BodyItem, FunctionDef}; +use cpp_semantics::{ + BodyItem, BranchCase, FunctionDef, FunctionId, FunctionKind, GuardExpression, LoopKind, +}; + +use crate::clang_adapter::scope::callable_scope; +use crate::clang_adapter::source_location::{is_in_main_file, parse_source_location}; +use crate::types::resolver::resolve_type; +use crate::visitor::SourceFileCache; +use crate::{context::VisitContext, AstVisitor}; pub struct FunctionVisitor; +/// Semantic roles assigned to the direct children of a supported libclang `IfStmt`. +struct IfParts<'tu> { + condition: Entity<'tu>, + then_body: Entity<'tu>, + else_body: Option>, +} + impl AstVisitor for FunctionVisitor { fn visit(ctx: &mut VisitContext, entity: Entity) { - if let Some(func_def) = Self::extract_function_def(entity) { - ctx.functions.push(func_def); - } + let mut source_files = SourceFileCache::default(); + Self::visit_with_source_files(ctx, &mut source_files, entity); } } impl FunctionVisitor { + /// Extracts a callable using source-text resources owned by the traversal. + pub(crate) fn visit_with_source_files( + ctx: &mut VisitContext, + source_files: &mut SourceFileCache, + entity: Entity, + ) { + if let Some(func_def) = Self::extract_function_def(source_files, entity) { + ctx.functions.push(func_def); + } + } + // ── Top-level extraction ────────────────────────────────────────────────── - fn extract_function_def(entity: Entity) -> Option { - let Some(body_node) = Self::get_method_body(entity) else { + fn extract_function_def( + source_files: &mut SourceFileCache, + entity: Entity, + ) -> Option { + if !is_in_main_file(&entity) { log::debug!( - "skipping method '{}': no compound statement body (declaration-only?)", + "skipping callable '{}': not located in the main file", entity.get_name().unwrap_or_default() ); return None; - }; + } - if !entity - .get_location() - .map(|loc| loc.is_in_main_file()) - .unwrap_or(false) - { + let Some(id) = Self::extract_function_id(&entity) else { log::debug!( - "skipping method '{}': not located in the main file", + "skipping callable '{}': no supported function identity", entity.get_name().unwrap_or_default() ); return None; - } + }; - let Some(method_name) = entity.get_name() else { - log::debug!("skipping method: entity has no name"); + let Some(kind) = Self::extract_function_kind(&entity) else { + log::debug!( + "skipping callable '{}': unsupported callable kind {:?}", + id.qualified_name(), + entity.get_kind() + ); return None; }; - let class_name = entity - .get_semantic_parent() - .and_then(|p| p.get_name()) - .unwrap_or_default(); - if class_name.is_empty() { + let Some(body) = Self::process_function_body(source_files, entity, &id) else { log::debug!( - "skipping method '{}': owning class/struct has no name", - method_name + "skipping callable '{}': no compound statement body (declaration-only?)", + id.qualified_name() ); return None; - } + }; - let body = Self::process_scope(body_node, &class_name); - let return_type = entity - .get_result_type() - .map(|t| t.get_display_name()) - .unwrap_or_else(|| "?".to_string()); + let return_type = if matches!(kind, FunctionKind::Constructor | FunctionKind::Destructor) { + None + } else { + entity.get_result_type().map(|t| resolve_type(&t)) + }; Some(FunctionDef { - class: class_name, - name: method_name, + id, + kind, return_type, body, }) @@ -87,10 +108,11 @@ impl FunctionVisitor { // ── AST navigation helpers ──────────────────────────────────────────────── - fn get_method_body(entity: Entity) -> Option { - Self::get_children(entity) - .into_iter() - .find(|c| c.get_kind() == EntityKind::CompoundStmt) + fn extract_function_id(entity: &Entity) -> Option { + Some(FunctionId { + scope: callable_scope(entity)?, + name: entity.get_name()?, + }) } fn get_children(entity: Entity) -> Vec { @@ -102,26 +124,45 @@ impl FunctionVisitor { v } - /// Walk down through wrapper nodes to find the first non-empty name. - /// Used to extract condition variable names from an IfStmt condition child. - fn extract_first_name(entity: Entity) -> String { - if let Some(name) = entity.get_name() { - if !name.is_empty() { - return name; - } - } - for child in Self::get_children(entity) { - let name = Self::extract_first_name(child); - if !name.is_empty() { - return name; - } + /// Returns an expression's original source-range text when available. + /// + /// Libclang locations expose byte offsets into the source file, so this + /// preserves the author's whitespace and operator spelling. + fn extract_expression_text(source_files: &mut SourceFileCache, entity: Entity) -> String { + entity + .get_range() + .and_then(|range| { + let start = range.get_start().get_file_location(); + let end = range.get_end().get_file_location(); + let file = start.file?; + let source = source_files.get(&file.get_path())?; + let start_offset = start.offset as usize; + let end_offset = end.offset as usize; + + source + .get(start_offset..end_offset) + .map(|bytes| String::from_utf8_lossy(bytes).into_owned()) + }) + .unwrap_or_default() + } + + fn extract_function_kind(entity: &Entity) -> Option { + match entity.get_kind() { + EntityKind::FunctionDecl => Some(FunctionKind::Free), + EntityKind::Method => Some(if entity.is_static_method() { + FunctionKind::StaticMethod + } else { + FunctionKind::Method + }), + EntityKind::Constructor => Some(FunctionKind::Constructor), + EntityKind::Destructor => Some(FunctionKind::Destructor), + EntityKind::ConversionFunction => Some(FunctionKind::Conversion), + _ => None, } - String::new() } - /// If `call_expr` is a call to a method owned by a class OTHER than `owner`, - /// return `(callee_class, method_name)` (or `"constructor"` for constructors). - fn cross_class_call_name(call_expr: Entity, owner: &str) -> Option<(String, String)> { + /// Resolves a call expression to its semantic callable target. + fn extract_call_target(call_expr: Entity) -> Option { // Direct reference works for simple `obj.method()` calls. // For virtual/pointer calls (`ptr->method()`), the reference lives on the // MemberRefExpr child — fall back to that when the direct lookup returns None. @@ -131,152 +172,345 @@ impl FunctionVisitor { .find(|c| c.get_kind() == EntityKind::MemberRefExpr) .and_then(|c| c.get_reference()) })?; - let parent = resolved.get_semantic_parent()?; - - let is_class_like = matches!( - parent.get_kind(), - EntityKind::ClassDecl - | EntityKind::StructDecl - | EntityKind::ClassTemplate - | EntityKind::ClassTemplatePartialSpecialization // | EntityKind::ClassTemplateSpecialization - ); - if !is_class_like { - return None; + + Self::extract_function_kind(&resolved)?; + Self::extract_function_id(&resolved) + } + + fn is_cross_owner_call(caller: &FunctionId, callee: &FunctionId) -> bool { + callee.scope != caller.scope + } + + // ── Scope/branch processors ─────────────────────────────────────────────── + + /// Locates a callable's compound body and processes its statements. + fn process_function_body( + source_files: &mut SourceFileCache, + function: Entity, + caller: &FunctionId, + ) -> Option> { + let body = Self::get_children(function) + .into_iter() + .find(|child| child.get_kind() == EntityKind::CompoundStmt)?; + + Some(Self::process_compound(source_files, body, caller)) + } + + /// Processes the direct statements of a `CompoundStmt` in source order. + fn process_compound( + source_files: &mut SourceFileCache, + compound: Entity, + caller: &FunctionId, + ) -> Vec { + Self::get_children(compound) + .into_iter() + .flat_map(|statement| Self::process_statement(source_files, statement, caller)) + .collect() + } + + /// Processes one statement, preserving nested control-flow structure. + fn process_statement( + source_files: &mut SourceFileCache, + entity: Entity, + caller: &FunctionId, + ) -> Vec { + match entity.get_kind() { + EntityKind::CompoundStmt => Self::process_compound(source_files, entity, caller), + EntityKind::IfStmt => Self::process_if(source_files, entity, caller), + EntityKind::ForStmt | EntityKind::WhileStmt | EntityKind::DoStmt => { + Self::process_loop(source_files, entity, caller) + } + _ => Self::collect_nested_calls(entity, caller), } + } - let parent_name = parent.get_name().unwrap_or_default(); - if parent_name.is_empty() || parent_name == owner { - return None; + /// Turns an IfStmt into one [`BodyItem::Branch`] with ordered cases. + /// + /// `else if` chains are flattened into cases, while an `else` that contains + /// a nested `if` remains a final else case containing a nested Branch. + fn process_if( + source_files: &mut SourceFileCache, + if_entity: Entity, + caller: &FunctionId, + ) -> Vec { + match Self::collect_branch_cases(source_files, if_entity, caller) { + Some(cases) => vec![BodyItem::Branch { cases }], + None => Self::process_if_fallback(source_files, if_entity, caller), } + } - if matches!( - resolved.get_kind(), - EntityKind::Constructor | EntityKind::Destructor - ) { - return Some((parent_name, "constructor".to_string())); + /// Collects the ordered cases of an if/else-if/else chain. + fn collect_branch_cases( + source_files: &mut SourceFileCache, + if_entity: Entity, + caller: &FunctionId, + ) -> Option> { + let parts = Self::split_if_parts(if_entity)?; + let mut cases = vec![BranchCase { + guard: Some(Self::extract_guard_expression( + source_files, + parts.condition, + caller, + )), + body: Self::process_statement(source_files, parts.then_body, caller), + source_location: parse_source_location(&if_entity), + }]; + + if let Some(else_body) = parts.else_body { + if else_body.get_kind() == EntityKind::IfStmt { + cases.extend(Self::collect_branch_cases(source_files, else_body, caller)?); + } else { + cases.push(BranchCase { + guard: None, + body: Self::process_statement(source_files, else_body, caller), + source_location: parse_source_location(&else_body), + }); + } } - resolved.get_name().map(|n| (parent_name, n)) + Some(cases) } - // ── Scope/branch processors ─────────────────────────────────────────────── - - /// Walk the subtree of `entity` collecting cross-class calls as [`BodyItem::Call`] - /// entries, skipping if/loop boundaries. Post-order on `CallExpr`: argument - /// calls appear before the outer call (execution order). - fn collect_calls_no_if(entity: Entity, owner: &str, out: &mut Vec) { - for child in Self::get_children(entity) { - match child.get_kind() { - EntityKind::IfStmt - | EntityKind::ForStmt - | EntityKind::WhileStmt - | EntityKind::DoStmt => {} - EntityKind::CallExpr => { - Self::collect_calls_no_if(child, owner, out); - if let Some((callee, name)) = Self::cross_class_call_name(child, owner) { - out.push(BodyItem::Call { callee, name }); - } - } - _ => Self::collect_calls_no_if(child, owner, out), + /// Maps the supported direct-child layout of an `IfStmt` to semantic roles. + /// + /// The current layout is `[condition, then_body, else_body?]`. More complex + /// forms, such as C++17 `if` statements with an initializer, use the + /// conservative no-data-loss fallback until their child layout is modeled. + fn split_if_parts(if_entity: Entity<'_>) -> Option> { + let children = Self::get_children(if_entity); + + match children.as_slice() { + [condition, then_body] => Some(IfParts { + condition: *condition, + then_body: *then_body, + else_body: None, + }), + [condition, then_body, else_body] => Some(IfParts { + condition: *condition, + then_body: *then_body, + else_body: Some(*else_body), + }), + _ => { + log::warn!( + "using fallback for IfStmt with unsupported direct-child layout: {} children", + children.len() + ); + None } } } - /// Process a `CompoundStmt` (or any scope entity) and return an ordered list of - /// [`BodyItem`]s that reflects the source execution order: calls, branches and - /// loops appear interleaved exactly as they do in the code. - fn process_scope(entity: Entity, owner: &str) -> Vec { - let mut body: Vec = Vec::new(); + /// Preserves reachable nested calls when an `IfStmt` layout is unsupported. + /// + /// The fallback deliberately does not invent a condition or branch shape; + /// it traverses all direct children so an unsupported cursor never causes + /// its entire subtree to disappear from the extracted model. + fn process_if_fallback( + source_files: &mut SourceFileCache, + if_entity: Entity, + caller: &FunctionId, + ) -> Vec { + log::warn!( + "falling back to unstructured processing for IfStmt at {:?}", + parse_source_location(&if_entity) + ); - entity.visit_children(|child, _| { - match child.get_kind() { - EntityKind::IfStmt => { - Self::process_if(child, owner, &mut body); - clang::EntityVisitResult::Continue + Self::get_children(if_entity) + .into_iter() + .flat_map(|child| Self::process_statement(source_files, child, caller)) + .collect() + } + + /// Extracts a condition as a tree that preserves `&&`, `||`, and `!` + /// short-circuit semantics. Other expressions remain source-backed leaves. + fn extract_guard_expression( + source_files: &mut SourceFileCache, + entity: Entity, + caller: &FunctionId, + ) -> GuardExpression { + match entity.get_kind() { + EntityKind::CallExpr => { + if let Some(target) = Self::extract_call_target(entity) + .filter(|target| Self::is_cross_owner_call(caller, target)) + { + return GuardExpression::Call { + target: target.qualified_name(), + text: Self::extract_expression_text(source_files, entity), + source_location: parse_source_location(&entity), + }; } - EntityKind::ForStmt | EntityKind::WhileStmt | EntityKind::DoStmt => { - body.push(Self::process_loop(child, owner)); - clang::EntityVisitResult::Continue + } + EntityKind::UnaryOperator if Self::has_leading_operator(entity, "!") => { + if let Some(expression) = Self::get_children(entity).into_iter().next() { + return GuardExpression::Not { + expression: Box::new(Self::extract_guard_expression( + source_files, + expression, + caller, + )), + }; } - EntityKind::CallExpr => { - // Post-order: emit argument calls before the outer call. - Self::collect_calls_no_if(child, owner, &mut body); - if let Some((callee, name)) = Self::cross_class_call_name(child, owner) { - body.push(BodyItem::Call { callee, name }); + } + EntityKind::BinaryOperator => { + let children = Self::get_children(entity); + if let [left, right] = children.as_slice() { + if let Some(operator) = Self::logical_operator(entity, *left, *right) { + return Self::combine_guard_expressions( + operator, + Self::extract_guard_expression(source_files, *left, caller), + Self::extract_guard_expression(source_files, *right, caller), + ); } - clang::EntityVisitResult::Continue } - _ => { - Self::collect_calls_no_if(child, owner, &mut body); - clang::EntityVisitResult::Continue + } + EntityKind::ParenExpr | EntityKind::UnexposedExpr => { + let children = Self::get_children(entity); + if let [expression] = children.as_slice() { + return Self::extract_guard_expression(source_files, *expression, caller); } } - }); - - body - } - - /// Turn an IfStmt into one or more [`BodyItem::Branch`] entries (one per arm). - fn process_if(if_entity: Entity, owner: &str, out: &mut Vec) { - let parts = Self::get_children(if_entity); - // IfStmt children: [condition_expr, then_body, (else_body)?] - let cond_text = parts - .first() - .map(|&c| Self::extract_first_name(c)) - .unwrap_or_default(); + _ => {} + } - // Collect any cross-class calls embedded inside the condition expression - // (e.g. `if (!plugin->WaitUntilLoaded(...))` — the call is in the condition, - // not the body, so it must be captured here before the Branch is created). - if let Some(&cond_ent) = parts.first() { - Self::collect_calls_no_if(cond_ent, owner, out); + GuardExpression::Opaque { + text: Self::extract_expression_text(source_files, entity), + source_location: parse_source_location(&entity), } + } - if let Some(&then_ent) = parts.get(1) { - out.push(BodyItem::Branch { - condition: cond_text, - body: Self::process_body(then_ent, owner), - }); + fn combine_guard_expressions( + operator: &str, + left: GuardExpression, + right: GuardExpression, + ) -> GuardExpression { + match operator { + "&&" => GuardExpression::And { + expressions: Self::flatten_guard_expressions(left, right, |expression| { + matches!(expression, GuardExpression::And { .. }) + }), + }, + "||" => GuardExpression::Or { + expressions: Self::flatten_guard_expressions(left, right, |expression| { + matches!(expression, GuardExpression::Or { .. }) + }), + }, + _ => unreachable!("only logical operators are combined"), } + } - if let Some(&else_ent) = parts.get(2) { - if else_ent.get_kind() == EntityKind::IfStmt { - Self::process_if(else_ent, owner, out); + fn flatten_guard_expressions( + left: GuardExpression, + right: GuardExpression, + is_same_operator: F, + ) -> Vec + where + F: Fn(&GuardExpression) -> bool, + { + let mut expressions = Vec::new(); + for expression in [left, right] { + if is_same_operator(&expression) { + match expression { + GuardExpression::And { + expressions: nested, + } + | GuardExpression::Or { + expressions: nested, + } => expressions.extend(nested), + _ => unreachable!("matching guard expression must be logical"), + } } else { - out.push(BodyItem::Branch { - condition: "else".to_string(), - body: Self::process_body(else_ent, owner), - }); + expressions.push(expression); } } + expressions + } + + /// Returns the logical operator located between a binary cursor's direct + /// left and right operands. This avoids interpreting an operator nested in + /// either operand, including template arguments and `operator&&` calls, as + /// the current cursor's operator. + fn logical_operator(entity: Entity, left: Entity, right: Entity) -> Option<&'static str> { + let left_end = left.get_range()?.get_end().get_file_location(); + let right_start = right.get_range()?.get_start().get_file_location(); + let file = left_end.file?; + + if right_start.file != Some(file) || left_end.offset > right_start.offset { + return None; + } + + entity + .get_range()? + .tokenize() + .into_iter() + .find_map(|token| { + let location = token.get_location().get_file_location(); + (location.file == Some(file) + && (left_end.offset..right_start.offset).contains(&location.offset)) + .then(|| match token.get_spelling().as_str() { + "&&" | "and" => Some("&&"), + "||" | "or" => Some("||"), + _ => None, + }) + .flatten() + }) + } + + fn has_leading_operator(entity: Entity, operator: &str) -> bool { + entity + .get_range() + .and_then(|range| range.tokenize().into_iter().next()) + .is_some_and(|token| { + token.get_spelling() == operator + || (operator == "!" && token.get_spelling() == "not") + }) } - /// Process a branch body, dispatching on kind. - fn process_body(entity: Entity, owner: &str) -> Vec { + /// Collects cross-owner calls in `entity`, without crossing control-flow + /// boundaries. Calls are emitted post-order, so nested calls precede their + /// enclosing call. This is structural nesting order, not a claim about the + /// evaluation order of sibling C++ call arguments. + fn collect_nested_calls(entity: Entity, caller: &FunctionId) -> Vec { match entity.get_kind() { - EntityKind::CompoundStmt => Self::process_scope(entity, owner), - EntityKind::IfStmt => { - let mut body = Vec::new(); - Self::process_if(entity, owner, &mut body); - body - } - _ => { - let mut body = Vec::new(); - Self::collect_calls_no_if(entity, owner, &mut body); - body + EntityKind::IfStmt + | EntityKind::ForStmt + | EntityKind::WhileStmt + | EntityKind::DoStmt => Vec::new(), + EntityKind::CallExpr => { + let mut calls: Vec<_> = Self::get_children(entity) + .into_iter() + .flat_map(|child| Self::collect_nested_calls(child, caller)) + .collect(); + + if let Some(target) = Self::extract_call_target(entity) { + if Self::is_cross_owner_call(caller, &target) { + calls.push(BodyItem::Call { + target: target.qualified_name(), + source_location: parse_source_location(&entity), + }); + } + } + + calls } + _ => Self::get_children(entity) + .into_iter() + .flat_map(|child| Self::collect_nested_calls(child, caller)) + .collect(), } } - /// Turn a loop statement into a [`BodyItem::Loop`]. - fn process_loop(loop_entity: Entity, owner: &str) -> BodyItem { + /// Turns a loop statement into its single [`BodyItem::Loop`] representation. + fn process_loop( + source_files: &mut SourceFileCache, + loop_entity: Entity, + caller: &FunctionId, + ) -> Vec { let kind = match loop_entity.get_kind() { - EntityKind::ForStmt => "for", - EntityKind::WhileStmt => "while", - EntityKind::DoStmt => "do_while", - _ => "unknown", - } - .to_string(); + EntityKind::ForStmt => LoopKind::For, + EntityKind::WhileStmt => LoopKind::While, + EntityKind::DoStmt => LoopKind::DoWhile, + _ => unreachable!("only loop statements are processed as loops"), + }; let parts = Self::get_children(loop_entity); let body_idx = match loop_entity.get_kind() { @@ -286,9 +520,13 @@ impl FunctionVisitor { let body = parts .get(body_idx) - .map(|&b| Self::process_body(b, owner)) + .map(|&b| Self::process_statement(source_files, b, caller)) .unwrap_or_default(); - BodyItem::Loop { kind, body } + vec![BodyItem::Loop { + kind, + body, + source_location: parse_source_location(&loop_entity), + }] } } diff --git a/cpp/libclang/src/visitor/src/lib.rs b/cpp/libclang/src/visitor/src/lib.rs index 6b8d0bf2..62c20c69 100644 --- a/cpp/libclang/src/visitor/src/lib.rs +++ b/cpp/libclang/src/visitor/src/lib.rs @@ -20,13 +20,11 @@ mod function_visitor; mod types; pub mod visitor; -pub use cpp_semantics::ResolvedType; -pub use sequence_logic::{BodyItem, FunctionDef}; +pub use cpp_semantics::{BodyItem, FunctionDef, ResolvedType}; pub use clang_adapter::source_filter::is_external_dependency_path; pub use class_visitor::ClassVisitor; pub use context::VisitContext; pub use enum_visitor::EnumVisitor; pub use function_visitor::FunctionVisitor; -pub use visitor::AstVisitor; -pub use visitor::Visitor; +pub use visitor::{AstVisitor, Visitor}; diff --git a/cpp/libclang/src/visitor/src/visitor.rs b/cpp/libclang/src/visitor/src/visitor.rs index 5fcd6410..389904b1 100644 --- a/cpp/libclang/src/visitor/src/visitor.rs +++ b/cpp/libclang/src/visitor/src/visitor.rs @@ -11,7 +11,11 @@ // SPDX-License-Identifier: Apache-2.0 // ******************************************************************************* +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + use clang::{Entity, EntityKind}; +use log::warn; use crate::clang_adapter::scope::namespace_id; use crate::clang_adapter::source_filter; @@ -24,13 +28,36 @@ pub trait AstVisitor { fn visit(ctx: &mut VisitContext, entity: Entity); } +/// Per-traversal cache for source-file contents. +/// +/// The cache belongs to `Visitor` because it is temporary traversal state, +/// rather than part of the extracted semantic model. +#[derive(Default)] +pub(crate) struct SourceFileCache { + files: HashMap>>, +} + +impl SourceFileCache { + /// Returns source bytes, loading each path at most once during traversal. + pub(crate) fn get(&mut self, path: &Path) -> Option<&[u8]> { + self.files + .entry(path.to_path_buf()) + .or_insert_with(|| std::fs::read(path).ok()) + .as_deref() + } +} + pub struct Visitor<'a> { ctx: &'a mut VisitContext, + source_files: SourceFileCache, } impl<'a> Visitor<'a> { pub fn new(ctx: &'a mut VisitContext) -> Self { - Self { ctx } + Self { + ctx, + source_files: SourceFileCache::default(), + } } pub fn visit(&mut self, entity: Entity) { @@ -49,12 +76,20 @@ impl<'a> Visitor<'a> { } EntityKind::ClassTemplate | EntityKind::ClassTemplatePartialSpecialization => { ClassVisitor::visit(self.ctx, entity); - // ClassTemplate parsing already processes all members, - // so skip generic child recursion to avoid double-processing. - return; } - EntityKind::Method => FunctionVisitor::visit(self.ctx, entity), EntityKind::EnumDecl => EnumVisitor::visit(self.ctx, entity), + EntityKind::FunctionDecl | EntityKind::Method => { + FunctionVisitor::visit_with_source_files(self.ctx, &mut self.source_files, entity); + } + EntityKind::FunctionTemplate => { + // TBD: Handle function templates if needed + } + EntityKind::Constructor | EntityKind::Destructor | EntityKind::ConversionFunction => { + warn!( + "Ignoring constructor, destructor, or conversion function: {:?}", + entity + ); + } _ => {} } diff --git a/tools/metamodel/sequence/sequence_logic.rs b/tools/metamodel/sequence/sequence_logic.rs index 4697c14b..4e8ce86c 100644 --- a/tools/metamodel/sequence/sequence_logic.rs +++ b/tools/metamodel/sequence/sequence_logic.rs @@ -15,32 +15,6 @@ use serde::{Deserialize, Serialize}; pub use source_location::SourceLocation; use std::sync::Arc; -/// A single item inside a function/branch/loop body, emitted in execution order. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum BodyItem { - /// A cross-class method call. - Call { callee: String, name: String }, - /// One arm of an if / else-if / else. The `condition` field is the guard - /// expression text, or `"else"` for an unconditional else arm. - Branch { - condition: String, - body: Vec, - }, - /// A for / while / do-while loop. - Loop { kind: String, body: Vec }, -} - -/// Represents a class method definition extracted from C++ source. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FunctionDef { - pub class: String, - pub name: String, - pub return_type: String, - /// Method body items in execution order (calls, branches, loops). - pub body: Vec, -} - /// For a PlantUML sequence diagram, this is the resolved participant identifier /// (typically the alias if present, otherwise the display name). /// From d921517f203df41177ce8ded08180f0de7f3bd81 Mon Sep 17 00:00:00 2001 From: Melody Ma Date: Tue, 1 Sep 2026 16:31:27 +0800 Subject: [PATCH 2/2] Integration test for if branch in functions --- .../free_function_identity/BUILD | 25 ++ .../free_function_identity/expected.json | 47 +++ .../free_function_identity/functions.cpp | 33 ++ .../free_function_identity/run_test.rs | 18 + .../function_cases/guard_associativity/BUILD | 25 ++ .../guard_associativity/expected.json | 310 ++++++++++++++++++ .../guard_associativity/flow.cpp | 68 ++++ .../guard_associativity/run_test.rs | 19 ++ .../function_cases/if_complex_condition/BUILD | 25 ++ .../if_complex_condition/expected.json | 82 +++++ .../if_complex_condition/flow.cpp | 25 ++ .../if_complex_condition/run_test.rs | 19 ++ .../function_cases/if_else_chain/BUILD | 25 ++ .../if_else_chain/expected.json | 92 ++++++ .../function_cases/if_else_chain/flow.cpp | 38 +++ .../function_cases/if_else_chain/run_test.rs | 19 ++ .../if_initializer_fallback/BUILD | 26 ++ .../if_initializer_fallback/expected.json | 45 +++ .../if_initializer_fallback/flow.cpp | 26 ++ .../if_initializer_fallback/run_test.rs | 19 ++ .../function_cases/if_without_braces/BUILD | 25 ++ .../if_without_braces/expected.json | 67 ++++ .../function_cases/if_without_braces/flow.cpp | 24 ++ .../if_without_braces/run_test.rs | 19 ++ .../function_cases/if_without_else/BUILD | 25 ++ .../if_without_else/expected.json | 50 +++ .../function_cases/if_without_else/flow.cpp | 22 ++ .../if_without_else/run_test.rs | 19 ++ .../function_cases/nested_if/BUILD | 25 ++ .../function_cases/nested_if/expected.json | 88 +++++ .../function_cases/nested_if/flow.cpp | 27 ++ .../function_cases/nested_if/run_test.rs | 19 ++ 32 files changed, 1396 insertions(+) create mode 100644 cpp/libclang/integration_test/function_cases/free_function_identity/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/free_function_identity/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp create mode 100644 cpp/libclang/integration_test/function_cases/free_function_identity/run_test.rs create mode 100644 cpp/libclang/integration_test/function_cases/guard_associativity/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/guard_associativity/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp create mode 100644 cpp/libclang/integration_test/function_cases/guard_associativity/run_test.rs create mode 100644 cpp/libclang/integration_test/function_cases/if_complex_condition/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp create mode 100644 cpp/libclang/integration_test/function_cases/if_complex_condition/run_test.rs create mode 100644 cpp/libclang/integration_test/function_cases/if_else_chain/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/if_else_chain/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp create mode 100644 cpp/libclang/integration_test/function_cases/if_else_chain/run_test.rs create mode 100644 cpp/libclang/integration_test/function_cases/if_initializer_fallback/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp create mode 100644 cpp/libclang/integration_test/function_cases/if_initializer_fallback/run_test.rs create mode 100644 cpp/libclang/integration_test/function_cases/if_without_braces/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/if_without_braces/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp create mode 100644 cpp/libclang/integration_test/function_cases/if_without_braces/run_test.rs create mode 100644 cpp/libclang/integration_test/function_cases/if_without_else/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/if_without_else/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp create mode 100644 cpp/libclang/integration_test/function_cases/if_without_else/run_test.rs create mode 100644 cpp/libclang/integration_test/function_cases/nested_if/BUILD create mode 100644 cpp/libclang/integration_test/function_cases/nested_if/expected.json create mode 100644 cpp/libclang/integration_test/function_cases/nested_if/flow.cpp create mode 100644 cpp/libclang/integration_test/function_cases/nested_if/run_test.rs diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/BUILD b/cpp/libclang/integration_test/function_cases/free_function_identity/BUILD new file mode 100644 index 00000000..63de8b23 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/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 = "free_function_identity", + srcs = glob(["*.cpp"]), + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_free_function_identity", + expected_output = ["expected.json"], + target = ":free_function_identity", +) diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/expected.json b/cpp/libclang/integration_test/function_cases/free_function_identity/expected.json new file mode 100644 index 00000000..f24885bc --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/expected.json @@ -0,0 +1,47 @@ +{ + "functions": [ + { + "id": { + "name": "global_value", + "scope": "Global" + }, + "kind": "Free", + "return_type": { + "Builtin": "int" + }, + "body": [] + }, + { + "id": { + "name": "run", + "scope": { + "Namespace": [ + "app" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [] + }, + { + "id": { + "name": "enabled", + "scope": { + "Namespace": [ + "app", + "internal" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "bool" + }, + "body": [] + } + ], + "types": {} +} diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp b/cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp new file mode 100644 index 00000000..df8ab3a9 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/functions.cpp @@ -0,0 +1,33 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +void declared_only(); + +int global_value() { + return 1; +} + +namespace app +{ + +void run() {} + +namespace internal +{ + +bool enabled() { + return true; +} + +} // namespace internal +} // namespace app diff --git a/cpp/libclang/integration_test/function_cases/free_function_identity/run_test.rs b/cpp/libclang/integration_test/function_cases/free_function_identity/run_test.rs new file mode 100644 index 00000000..8c82e33e --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/free_function_identity/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_free_function_identity() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/guard_associativity/BUILD b/cpp/libclang/integration_test/function_cases/guard_associativity/BUILD new file mode 100644 index 00000000..6edccfac --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/guard_associativity/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 = "guard_associativity", + srcs = ["flow.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_guard_associativity", + expected_output = ["expected.json"], + target = ":guard_associativity", +) diff --git a/cpp/libclang/integration_test/function_cases/guard_associativity/expected.json b/cpp/libclang/integration_test/function_cases/guard_associativity/expected.json new file mode 100644 index 00000000..677d3592 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/guard_associativity/expected.json @@ -0,0 +1,310 @@ +{ + "functions": [ + { + "id": { + "name": "guard_associativity", + "scope": { + "Namespace": [ + "flow" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "cases": [ + { + "guard": { + "type": "and", + "expressions": [ + { + "type": "call", + "target": "first", + "text": "first()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 39 + } + }, + { + "type": "call", + "target": "second", + "text": "second()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 39 + } + }, + { + "type": "call", + "target": "third", + "text": "third()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 39 + } + } + ] + }, + "body": [ + { + "target": "handle_and", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 40 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 39 + } + } + ], + "type": "branch" + }, + { + "cases": [ + { + "guard": { + "type": "and", + "expressions": [ + { + "type": "or", + "expressions": [ + { + "type": "call", + "target": "first", + "text": "first()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 44 + } + }, + { + "type": "call", + "target": "second", + "text": "second()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 44 + } + } + ] + }, + { + "type": "call", + "target": "third", + "text": "third()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 44 + } + } + ] + }, + "body": [ + { + "target": "handle_or", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 45 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 44 + } + } + ], + "type": "branch" + }, + { + "cases": [ + { + "guard": { + "type": "or", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "call", + "target": "first", + "text": "first()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 49 + } + }, + { + "type": "call", + "target": "second", + "text": "second()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 49 + } + } + ] + }, + { + "type": "call", + "target": "third", + "text": "third()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 49 + } + } + ] + }, + "body": [ + { + "target": "handle_mixed", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 50 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 49 + } + } + ], + "type": "branch" + }, + { + "cases": [ + { + "guard": { + "type": "and", + "expressions": [ + { + "type": "call", + "target": "first", + "text": "first()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 54 + } + }, + { + "type": "call", + "target": "second", + "text": "second()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 54 + } + } + ] + }, + "body": [ + { + "target": "handle_alternative", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 55 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 54 + } + } + ], + "type": "branch" + }, + { + "cases": [ + { + "guard": { + "type": "opaque", + "text": "check_template() == third()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 59 + } + }, + "body": [ + { + "target": "handle_template_operand", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 60 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 59 + } + } + ], + "type": "branch" + }, + { + "cases": [ + { + "guard": { + "type": "opaque", + "text": "left == operator&&(first_flag(), second_flag())", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 64 + } + }, + "body": [ + { + "target": "handle_operator_function", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 65 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 64 + } + } + ], + "type": "branch" + } + ] + } + ], + "types": { + "Flag": { + "id": "Flag", + "name": "Flag", + "enclosing_namespace_id": null, + "stereotypes": [], + "entity_type": "Struct", + "type_aliases": [], + "variables": [], + "methods": [], + "template_parameters": null, + "enum_literals": [], + "relationships": [], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp", + "line": 30 + } + } + } +} diff --git a/cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp b/cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp new file mode 100644 index 00000000..be5e7727 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/guard_associativity/flow.cpp @@ -0,0 +1,68 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +bool first(); +bool second(); +bool third(); +void handle_and(); +void handle_or(); +void handle_mixed(); +void handle_alternative(); +void handle_template_operand(); +void handle_operator_function(); + +template +bool check_template(); + +constexpr bool template_first = true; +constexpr bool template_second = true; + +struct Flag {}; +Flag first_flag(); +Flag second_flag(); +bool left; +bool operator&&(Flag, Flag); + +namespace flow { +void guard_associativity() { + // Flattens a parenthesized right-associative logical-and chain. + if (first() && (second() && third())) { + handle_and(); + } + + // Preserves an or expression nested in a logical-and expression. + if ((first() || second()) && third()) { + handle_or(); + } + + // Uses C++ precedence for an unparenthesized && / || expression. + if (first() && second() || third()) { + handle_mixed(); + } + + // Recognizes C++'s alternative logical-and token. + if (first() and second()) { + handle_alternative(); + } + + // Ignores a logical token inside a template argument of a non-logical root. + if (check_template() == third()) { + handle_template_operand(); + } + + // Does not mistake operator&& in the right-hand CallExpr for the outer ==. + if (left == operator&&(first_flag(), second_flag())) { + handle_operator_function(); + } +} +} // namespace flow diff --git a/cpp/libclang/integration_test/function_cases/guard_associativity/run_test.rs b/cpp/libclang/integration_test/function_cases/guard_associativity/run_test.rs new file mode 100644 index 00000000..29331963 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/guard_associativity/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_guard_associativity() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/if_complex_condition/BUILD b/cpp/libclang/integration_test/function_cases/if_complex_condition/BUILD new file mode 100644 index 00000000..c902fab9 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_complex_condition/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 = "if_complex_condition", + srcs = ["flow.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_if_complex_condition", + expected_output = ["expected.json"], + target = ":if_complex_condition", +) diff --git a/cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json b/cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json new file mode 100644 index 00000000..84bf6a58 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_complex_condition/expected.json @@ -0,0 +1,82 @@ +{ + "functions": [ + { + "id": { + "name": "complex_condition", + "scope": { + "Namespace": [ + "flow" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "cases": [ + { + "guard": { + "type": "and", + "expressions": [ + { + "type": "call", + "target": "is_ready", + "text": "is_ready()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 21 + } + }, + { + "type": "or", + "expressions": [ + { + "type": "call", + "target": "is_allowed", + "text": "is_allowed()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 21 + } + }, + { + "type": "not", + "expression": { + "type": "call", + "target": "has_permission", + "text": "has_permission()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 21 + } + } + } + ] + } + ] + }, + "body": [ + { + "target": "handle_complex", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 22 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp", + "line": 21 + } + } + ], + "type": "branch" + } + ] + } + ], + "types": {} +} diff --git a/cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp b/cpp/libclang/integration_test/function_cases/if_complex_condition/flow.cpp new file mode 100644 index 00000000..f9f9cdff --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_complex_condition/flow.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 + ********************************************************************************/ + +bool is_ready(); +bool is_allowed(); +bool has_permission(); +void handle_complex(); + +namespace flow { +void complex_condition() { + if (is_ready() && (is_allowed() || !has_permission())) { + handle_complex(); + } +} +} // namespace flow diff --git a/cpp/libclang/integration_test/function_cases/if_complex_condition/run_test.rs b/cpp/libclang/integration_test/function_cases/if_complex_condition/run_test.rs new file mode 100644 index 00000000..b6aee507 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_complex_condition/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_if_complex_condition() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/if_else_chain/BUILD b/cpp/libclang/integration_test/function_cases/if_else_chain/BUILD new file mode 100644 index 00000000..f8611ed3 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_else_chain/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 = "if_else_chain", + srcs = ["flow.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_if_else_chain", + expected_output = ["expected.json"], + target = ":if_else_chain", +) diff --git a/cpp/libclang/integration_test/function_cases/if_else_chain/expected.json b/cpp/libclang/integration_test/function_cases/if_else_chain/expected.json new file mode 100644 index 00000000..bee3509a --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_else_chain/expected.json @@ -0,0 +1,92 @@ +{ + "functions": [ + { + "id": { + "name": "evaluate", + "scope": { + "Namespace": [ + "flow" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "cases": [ + { + "guard": { + "type": "call", + "target": "is_ready", + "text": "is_ready()", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 24 + } + }, + "body": [ + { + "target": "handle_ready", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 26 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 24 + } + }, + { + "guard": { + "type": "opaque", + "text": "retry", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 28 + } + }, + "body": [ + { + "target": "handle_retry", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 30 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 28 + } + }, + { + "guard": null, + "body": [ + { + "target": "handle_failure", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 34 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp", + "line": 33 + } + } + ], + "type": "branch" + } + ] + } + ], + "types": {} +} diff --git a/cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp b/cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp new file mode 100644 index 00000000..68536f78 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_else_chain/flow.cpp @@ -0,0 +1,38 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +bool is_ready(); +void handle_ready(); +void handle_retry(); +void handle_failure(); + +namespace flow +{ + +void evaluate(bool retry) +{ + if (is_ready()) + { + handle_ready(); + } + else if (retry) + { + handle_retry(); + } + else + { + handle_failure(); + } +} + +} // namespace flow diff --git a/cpp/libclang/integration_test/function_cases/if_else_chain/run_test.rs b/cpp/libclang/integration_test/function_cases/if_else_chain/run_test.rs new file mode 100644 index 00000000..80b42d17 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_else_chain/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_if_else_chain() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/if_initializer_fallback/BUILD b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/BUILD new file mode 100644 index 00000000..ebc0446c --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/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 = "if_initializer_fallback", + srcs = ["flow.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_if_initializer_fallback", + expected_output = ["expected.json"], + extra_args = ["-std=c++17"], + target = ":if_initializer_fallback", +) diff --git a/cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json new file mode 100644 index 00000000..2f0a756b --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/expected.json @@ -0,0 +1,45 @@ +{ + "types": {}, + "functions": [ + { + "id": { + "scope": { + "Namespace": [ + "flow" + ] + }, + "name": "if_initializer_fallback" + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "type": "call", + "target": "initialize", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 20 + } + }, + { + "type": "call", + "target": "handle_ready", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 21 + } + }, + { + "type": "call", + "target": "handle_failure", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp", + "line": 23 + } + } + ] + } + ] +} diff --git a/cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.cpp new file mode 100644 index 00000000..561b5082 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/flow.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 + ********************************************************************************/ + +bool initialize(); +void handle_ready(); +void handle_failure(); + +namespace flow { +void if_initializer_fallback() { + if (bool ready = initialize(); ready) { + handle_ready(); + } else { + handle_failure(); + } +} +} // namespace flow diff --git a/cpp/libclang/integration_test/function_cases/if_initializer_fallback/run_test.rs b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/run_test.rs new file mode 100644 index 00000000..de50d80b --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_initializer_fallback/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_if_initializer_fallback() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/if_without_braces/BUILD b/cpp/libclang/integration_test/function_cases/if_without_braces/BUILD new file mode 100644 index 00000000..3f7ccbdc --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_braces/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 = "if_without_braces", + srcs = ["flow.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_if_without_braces", + expected_output = ["expected.json"], + target = ":if_without_braces", +) diff --git a/cpp/libclang/integration_test/function_cases/if_without_braces/expected.json b/cpp/libclang/integration_test/function_cases/if_without_braces/expected.json new file mode 100644 index 00000000..d8f1e993 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_braces/expected.json @@ -0,0 +1,67 @@ +{ + "functions": [ + { + "id": { + "name": "without_braces", + "scope": { + "Namespace": [ + "flow" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "cases": [ + { + "guard": { + "type": "opaque", + "text": "enabled", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 19 + } + }, + "body": [ + { + "target": "handle_unbraced", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 20 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 19 + } + }, + { + "guard": null, + "body": [ + { + "target": "handle_failure", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 22 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp", + "line": 22 + } + } + ], + "type": "branch" + } + ] + } + ], + "types": {} +} diff --git a/cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp b/cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp new file mode 100644 index 00000000..87bc37b8 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_braces/flow.cpp @@ -0,0 +1,24 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +void handle_unbraced(); +void handle_failure(); + +namespace flow { +void without_braces(bool enabled) { + if (enabled) + handle_unbraced(); + else + handle_failure(); +} +} // namespace flow diff --git a/cpp/libclang/integration_test/function_cases/if_without_braces/run_test.rs b/cpp/libclang/integration_test/function_cases/if_without_braces/run_test.rs new file mode 100644 index 00000000..54f0885f --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_braces/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_if_without_braces() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/if_without_else/BUILD b/cpp/libclang/integration_test/function_cases/if_without_else/BUILD new file mode 100644 index 00000000..0da7c571 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_else/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 = "if_without_else", + srcs = ["flow.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_if_without_else", + expected_output = ["expected.json"], + target = ":if_without_else", +) diff --git a/cpp/libclang/integration_test/function_cases/if_without_else/expected.json b/cpp/libclang/integration_test/function_cases/if_without_else/expected.json new file mode 100644 index 00000000..23186f8a --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_else/expected.json @@ -0,0 +1,50 @@ +{ + "functions": [ + { + "id": { + "name": "without_else", + "scope": { + "Namespace": [ + "flow" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "cases": [ + { + "guard": { + "type": "opaque", + "text": "enabled", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp", + "line": 18 + } + }, + "body": [ + { + "target": "handle_no_else", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp", + "line": 19 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp", + "line": 18 + } + } + ], + "type": "branch" + } + ] + } + ], + "types": {} +} diff --git a/cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp b/cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp new file mode 100644 index 00000000..4f0d34e5 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_else/flow.cpp @@ -0,0 +1,22 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +void handle_no_else(); + +namespace flow { +void without_else(bool enabled) { + if (enabled) { + handle_no_else(); + } +} +} // namespace flow diff --git a/cpp/libclang/integration_test/function_cases/if_without_else/run_test.rs b/cpp/libclang/integration_test/function_cases/if_without_else/run_test.rs new file mode 100644 index 00000000..19001950 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/if_without_else/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_if_without_else() { + run_parser_case(); +} diff --git a/cpp/libclang/integration_test/function_cases/nested_if/BUILD b/cpp/libclang/integration_test/function_cases/nested_if/BUILD new file mode 100644 index 00000000..9f2f60ca --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/nested_if/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_if", + srcs = ["flow.cpp"], + visibility = ["//cpp/libclang:__subpackages__"], +) + +cpp_parser_integration_test( + name = "test_nested_if", + expected_output = ["expected.json"], + target = ":nested_if", +) diff --git a/cpp/libclang/integration_test/function_cases/nested_if/expected.json b/cpp/libclang/integration_test/function_cases/nested_if/expected.json new file mode 100644 index 00000000..33a7c827 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/nested_if/expected.json @@ -0,0 +1,88 @@ +{ + "functions": [ + { + "id": { + "name": "nested_if", + "scope": { + "Namespace": [ + "flow" + ] + } + }, + "kind": "Free", + "return_type": { + "Builtin": "void" + }, + "body": [ + { + "cases": [ + { + "guard": { + "type": "opaque", + "text": "outer", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 19 + } + }, + "body": [ + { + "cases": [ + { + "guard": { + "type": "opaque", + "text": "inner", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 20 + } + }, + "body": [ + { + "target": "handle_nested", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 21 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 20 + } + } + ], + "type": "branch" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 19 + } + }, + { + "guard": null, + "body": [ + { + "target": "handle_outer_else", + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 24 + }, + "type": "call" + } + ], + "source_location": { + "file": "cpp/libclang/integration_test/function_cases/nested_if/flow.cpp", + "line": 23 + } + } + ], + "type": "branch" + } + ] + } + ], + "types": {} +} diff --git a/cpp/libclang/integration_test/function_cases/nested_if/flow.cpp b/cpp/libclang/integration_test/function_cases/nested_if/flow.cpp new file mode 100644 index 00000000..34fa4c6e --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/nested_if/flow.cpp @@ -0,0 +1,27 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +void handle_outer_else(); +void handle_nested(); + +namespace flow { +void nested_if(bool outer, bool inner) { + if (outer) { + if (inner) { + handle_nested(); + } + } else { + handle_outer_else(); + } +} +} // namespace flow diff --git a/cpp/libclang/integration_test/function_cases/nested_if/run_test.rs b/cpp/libclang/integration_test/function_cases/nested_if/run_test.rs new file mode 100644 index 00000000..71d734f3 --- /dev/null +++ b/cpp/libclang/integration_test/function_cases/nested_if/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_if() { + run_parser_case(); +}