use crate::call_match::{CppArgType, normalize_cpp_type_name};
use crate::compile_context::{CompiledLanguage, CppCompileContext};
use crate::declarations::cpp_displaced_preprocessor_branch;
#[cfg(test)]
use crate::declarations::cpp_displaced_preprocessor_terminator;
use crate::declarations::{
CppComparableNode, CppComparableParameter, CppComparableSlot, CppRecoveredExportClassIndex,
cpp_displaced_preprocessor_boundary, cpp_export_macro_token, cpp_template_term,
enclosing_cpp_namespace_nodes, macro_modifier_identifier, node_text, normalize_cpp_whitespace,
recovered_class_body_at, recovered_function_like_field_declarator,
recovered_pyobject_head_field,
};
use crate::graph::CppGraphSource;
use crate::graph::extractor::ScanCtx;
use crate::graph::syntax::{
SentinelInput, function_macro_replacement_span, object_macro_replacement_type_references,
parse_sentinel, qualified_name_parts,
};
use crate::graph_support::CppSource;
use crate::imports::{
IncludeTargetIndex, include_path_from_node, resolve_include_targets_with_index,
};
use crate::source_context::cpp_guard_set_to_runtime;
use crate::structural::is_cpp_preprocessor_conditional;
use crate::template_heads::{
CppTemplateBinding, CppTemplateContext, CppTemplateReconciliation,
cpp_declaration_template_context,
};
pub use brokk_bifrost_core::analyzer::cpp_facts::{
CppClassDeclarationStrength, CppEnumOwnerKind as EnumOwnerKind,
};
use brokk_bifrost_core::analyzer::cpp_facts::{
CppDeclaredFieldTypeFact as DeclaredFieldTypeFact,
CppStructuredAliasTarget as StructuredAliasTarget,
};
use brokk_bifrost_core::analyzer::fq_name::{FqName, SegmentKind, segment_interner};
use brokk_bifrost_core::analyzer::memo_attribution::{ReplayingMemo, ReplayingMemoEntry};
use brokk_bifrost_core::analyzer::model::{
CallableArity, CallableLinkage, CodeUnitType, CppFieldLinkage, CppTemplateExpression,
CppTemplateMetadata, CppTemplateParameterMetadata, CppTemplateTerm, Language, LanguageDialect,
SignatureMetadata, StructuredTypeName,
};
use brokk_bifrost_core::analyzer::pool_memo::{KeyedPoolSafeMemo, PoolSafeMemo};
use brokk_bifrost_core::analyzer::prepared_syntax::PreparedSyntaxTree;
#[cfg(test)]
use brokk_bifrost_core::analyzer::prepared_syntax::{PreparedSourceOrigin, PreparedSyntaxSource};
use brokk_bifrost_core::analyzer::project_reads::{OpaqueReadReceipt, ReadCapability};
use brokk_bifrost_core::analyzer::query_token::QueryToken;
use brokk_bifrost_core::analyzer::semantic_diagnostics::contains_node as node_range_contains;
use brokk_bifrost_core::analyzer::source_facts::SourceDeclarationId;
use brokk_bifrost_core::analyzer::structural::adapter_helpers::{field_name_in_parent, node_range};
use brokk_bifrost_core::analyzer::tree_walk::{
ParentIndex, child_by_field_or_named_child_of_kinds, children_iter, first_named_child_of_kind,
named_children_iter, node_for_exact_range, push_named_children_reversed,
single_named_child_matching,
};
use brokk_bifrost_core::analyzer::usages::common::same_node;
use brokk_bifrost_core::analyzer::usages::local_inference::LocalInferenceEngine;
use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile, Range};
use brokk_bifrost_core::cancellation::CancellationToken;
use brokk_bifrost_core::hash::{HashMap, HashSet};
#[cfg(test)]
use brokk_bifrost_core::text_utils::compute_line_starts;
use std::borrow::Cow;
#[cfg(any(test, feature = "test-support"))]
use std::cell::Cell;
use std::cell::OnceCell;
use std::cmp::Ordering as CmpOrdering;
use std::collections::BTreeSet;
use std::hash::Hash;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
use tree_sitter::{Node, Parser, Tree};
const PREPROCESSOR_CONTEXT_CACHE_CAP: usize = 8192;
const CALLABLE_METADATA_CACHE_CAP: usize = 512;
const MACRO_LEXICAL_INDEX_CACHE_CAP: usize = 64;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct PreprocessorContextNodeKey {
id: usize,
start_byte: usize,
end_byte: usize,
}
#[derive(Default)]
pub(crate) struct PreprocessorContextCache {
displaced_boundaries: HashMap<PreprocessorContextNodeKey, Option<usize>>,
displaced_branches: HashMap<PreprocessorContextNodeKey, Option<DisplacedBranchDirective>>,
}
#[derive(Clone, Copy)]
struct DisplacedBranchDirective {
end_byte: usize,
is_else: bool,
}
impl PreprocessorContextCache {
fn displaced_branch(&mut self, conditional: Node<'_>) -> Option<DisplacedBranchDirective> {
let key = PreprocessorContextNodeKey {
id: conditional.id(),
start_byte: conditional.start_byte(),
end_byte: conditional.end_byte(),
};
if let Some(directive) = self.displaced_branches.get(&key) {
return *directive;
}
let directive = cpp_displaced_preprocessor_branch(conditional).map(|directive| {
DisplacedBranchDirective {
end_byte: directive.end_byte(),
is_else: directive.kind() == "#else",
}
});
if self.displaced_branches.len() < PREPROCESSOR_CONTEXT_CACHE_CAP {
self.displaced_branches.insert(key, directive);
}
directive
}
fn displaced_boundary_end(&mut self, conditional: Node<'_>) -> Option<usize> {
let key = PreprocessorContextNodeKey {
id: conditional.id(),
start_byte: conditional.start_byte(),
end_byte: conditional.end_byte(),
};
if let Some(boundary) = self.displaced_boundaries.get(&key) {
return *boundary;
}
let boundary =
cpp_displaced_preprocessor_boundary(conditional).map(|boundary| boundary.end_byte);
if self.displaced_boundaries.len() < PREPROCESSOR_CONTEXT_CACHE_CAP {
self.displaced_boundaries.insert(key, boundary);
}
boundary
}
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static BOUNDED_VISIBILITY_DECLARATION_READ_COUNT: Cell<usize> = const { Cell::new(0) };
static BOUNDED_VISIBILITY_DEPENDENCY_AST_NODE_COUNT: Cell<usize> = const { Cell::new(0) };
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TargetKind {
Type,
Constructor,
FreeFunction,
Method,
GlobalField,
MemberField,
Macro,
}
pub enum LexicalTypeResolution {
Resolved {
unit: CodeUnit,
components: Vec<String>,
candidates: Vec<CodeUnit>,
},
Ambiguous,
Missing,
}
pub enum GuardedAliasSelection {
Proven {
unit: CodeUnit,
candidates: Vec<CodeUnit>,
},
Alternatives(Vec<CodeUnit>),
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum AliasArmEvidence {
Implied,
Compatible,
Contradicted,
}
#[derive(Clone, Copy)]
enum TypeCandidateResolution<'a> {
Canonical,
PreserveAlias,
PreserveTarget(&'a CodeUnit),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TypeCandidateFailure {
Ambiguous,
Unresolvable,
}
impl TypeCandidateFailure {
fn lexical_resolution(self) -> LexicalTypeResolution {
match self {
Self::Ambiguous => LexicalTypeResolution::Ambiguous,
Self::Unresolvable => LexicalTypeResolution::Missing,
}
}
}
pub enum LexicalCallableValueResolution {
Type(CodeUnit),
FreeFunction(CodeUnit),
Ambiguous,
Missing,
}
pub enum UsingEnumMemberResolution {
Resolved { owner: CodeUnit, member: CodeUnit },
Ambiguous,
Missing,
}
pub enum NamespaceValueResolution {
Resolved,
Ambiguous,
Missing,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OrdinaryMacroReferenceResolution {
Resolved(CodeUnit),
Ambiguous,
Missing,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RecoveredCReferenceRanges {
Complete(Vec<Range>),
LimitExceeded,
}
pub fn resolve_namespace_value(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
namespace: &str,
name: &str,
before_byte: usize,
) -> NamespaceValueResolution {
let mut matches = Vec::new();
for candidate in visibility.visible_identifier_candidates(file, name) {
if type_owner_of(analyzer, candidate).is_some()
|| candidate.package_name() != namespace
|| (candidate.source() == file
&& !analyzer
.ranges(candidate)
.iter()
.any(|range| range.start_byte < before_byte))
|| matches
.iter()
.any(|existing| same_visible_symbol(existing, candidate))
{
continue;
}
matches.push(candidate.clone());
if matches.len() > 1 {
return NamespaceValueResolution::Ambiguous;
}
}
matches
.pop()
.map(|_| NamespaceValueResolution::Resolved)
.unwrap_or(NamespaceValueResolution::Missing)
}
pub(crate) struct ScopedUsingEnumOwners {
scopes: Vec<Vec<CodeUnit>>,
}
pub(crate) struct SemanticUsingEnumOwners {
class_imports: HashMap<CodeUnit, Vec<CodeUnit>>,
namespace_imports: HashMap<Vec<String>, Vec<(usize, CodeUnit)>>,
}
pub(crate) enum SemanticUsingEnumMemberResolution {
Class(UsingEnumMemberResolution),
Namespace(UsingEnumMemberResolution),
Missing,
}
impl SemanticUsingEnumOwners {
pub(crate) fn new() -> Self {
Self {
class_imports: HashMap::default(),
namespace_imports: HashMap::default(),
}
}
pub fn import_class(&mut self, class: CodeUnit, enum_owner: CodeUnit) {
let imports = self.class_imports.entry(class).or_default();
if !imports
.iter()
.any(|existing| same_visible_symbol(existing, &enum_owner))
{
imports.push(enum_owner);
}
}
pub fn import_namespace(
&mut self,
namespace: Vec<String>,
declaration_byte: usize,
enum_owner: CodeUnit,
) {
let imports = self.namespace_imports.entry(namespace).or_default();
if !imports
.iter()
.any(|(_, existing)| same_visible_symbol(existing, &enum_owner))
{
imports.push((declaration_byte, enum_owner));
}
}
pub fn resolve_member(
&self,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
class: Option<&CodeUnit>,
namespace: &[String],
before_byte: usize,
name: &str,
) -> SemanticUsingEnumMemberResolution {
if let Some(class) = class
&& let Some((_, imports)) = self
.class_imports
.iter()
.find(|(owner, _)| same_visible_symbol(owner, class))
{
let resolution =
resolve_using_enum_member_for_owners(visibility, file, imports.iter(), name);
if !matches!(resolution, UsingEnumMemberResolution::Missing) {
return SemanticUsingEnumMemberResolution::Class(resolution);
}
}
for prefix_len in (0..=namespace.len()).rev() {
let Some(imports) = self.namespace_imports.get(&namespace[..prefix_len]) else {
continue;
};
let owners = imports
.iter()
.filter(|(declaration_byte, _)| *declaration_byte < before_byte)
.map(|(_, owner)| owner);
let resolution = resolve_using_enum_member_for_owners(visibility, file, owners, name);
if !matches!(resolution, UsingEnumMemberResolution::Missing) {
return SemanticUsingEnumMemberResolution::Namespace(resolution);
}
}
SemanticUsingEnumMemberResolution::Missing
}
}
fn resolve_using_enum_member_for_owners<'a>(
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
owners: impl IntoIterator<Item = &'a CodeUnit>,
name: &str,
) -> UsingEnumMemberResolution {
let mut matches: Vec<(CodeUnit, CodeUnit)> = Vec::new();
for owner in owners {
for member in visibility.visible_members_for_owner_name(file, owner, name) {
if !member.is_field()
|| matches.iter().any(|(existing_owner, existing_member)| {
same_visible_symbol(existing_owner, owner)
&& same_visible_symbol(existing_member, member)
})
{
continue;
}
matches.push((owner.clone(), member.clone()));
}
}
match matches.len() {
0 => UsingEnumMemberResolution::Missing,
1 => {
let (owner, member) = matches.pop().expect("one using-enum match");
UsingEnumMemberResolution::Resolved { owner, member }
}
_ => UsingEnumMemberResolution::Ambiguous,
}
}
impl ScopedUsingEnumOwners {
pub(crate) fn new() -> Self {
Self {
scopes: vec![Vec::new()],
}
}
pub fn enter_scope(&mut self) {
self.scopes.push(Vec::new());
}
pub fn exit_scope(&mut self) {
brokk_bifrost_core::analyzer::usages::exit_nested_scope(&mut self.scopes);
}
pub fn import(&mut self, owner: CodeUnit) {
let scope = self
.scopes
.last_mut()
.expect("using-enum scope stack is never empty");
if !scope
.iter()
.any(|existing| same_visible_symbol(existing, &owner))
{
scope.push(owner);
}
}
pub fn resolve_member(
&self,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
name: &str,
) -> UsingEnumMemberResolution {
for scope in self.scopes.iter().rev() {
let resolution =
resolve_using_enum_member_for_owners(visibility, file, scope.iter(), name);
if !matches!(resolution, UsingEnumMemberResolution::Missing) {
return resolution;
}
}
UsingEnumMemberResolution::Missing
}
}
#[derive(Clone)]
pub struct TargetSpec {
pub target: CodeUnit,
pub kind: TargetKind,
pub owner: Option<CodeUnit>,
pub member_name: String,
pub callable_arity: Option<CallableArity>,
pub activated_callable_arities: Vec<ActivatedCallableArity>,
pub param_types: Option<Vec<String>>,
pub enum_owner_kind: EnumOwnerKind,
pub owner_is_forward_declaration: bool,
pub callable_has_definition_body: bool,
}
#[derive(Clone, Copy)]
pub struct ActivatedCallableArity {
pub activation_byte: usize,
pub arity: CallableArity,
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct TypeScanKey {
target: LogicalSymbolKey,
member_name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct LogicalSymbolKey {
kind: CodeUnitType,
fq_name: String,
signature: Option<String>,
}
struct ResolvedTypeOwner {
unit: CodeUnit,
is_forward_declaration: bool,
}
impl TargetSpec {
pub fn type_scan_key(&self) -> Option<TypeScanKey> {
(self.kind == TargetKind::Type).then(|| TypeScanKey {
target: logical_symbol_key(&self.target),
member_name: self.member_name.clone(),
})
}
pub fn from_target(analyzer: &CppGraphSource<'_>, target: &CodeUnit) -> Option<Self> {
if target.is_class() {
return Some(Self::new(
target.clone(),
TargetKind::Type,
Some(target.clone()),
target.identifier().to_string(),
None,
None,
));
}
if target.is_field() {
let owner = type_owner_of(analyzer, target);
let kind = if owner.is_some() {
TargetKind::MemberField
} else {
TargetKind::GlobalField
};
let enum_owner_kind = owner
.as_ref()
.map(|owner| classify_enum_owner(analyzer, owner))
.unwrap_or(EnumOwnerKind::NonEnum);
let mut spec = Self::new(
target.clone(),
kind,
owner,
target.identifier().to_string(),
None,
None,
);
spec.enum_owner_kind = enum_owner_kind;
return Some(spec);
}
if target.is_function() {
let owner_resolution = target_type_owner_resolution(analyzer, target);
let owner_is_forward_declaration = owner_resolution
.as_ref()
.is_some_and(|owner| owner.is_forward_declaration);
let owner = owner_resolution.map(|owner| owner.unit);
let kind = if owner.as_ref().is_some_and(|owner| {
target.identifier() == owner.identifier()
|| analyzer
.cpp
.and_then(|cpp| cpp.template_metadata(owner))
.is_some_and(|metadata| metadata.primary_name == target.identifier())
}) {
TargetKind::Constructor
} else if owner.is_some() {
TargetKind::Method
} else {
TargetKind::FreeFunction
};
let mut spec = Self::new(
target.clone(),
kind,
owner,
target.identifier().to_string(),
cpp_callable_arity(analyzer, target),
cpp_callable_parameter_types(analyzer, target),
);
spec.owner_is_forward_declaration = owner_is_forward_declaration;
spec.callable_has_definition_body =
callable_target_has_definition_body(analyzer, target);
return Some(spec);
}
if target.is_macro() {
return Some(Self::new(
target.clone(),
TargetKind::Macro,
None,
target.identifier().to_string(),
None,
None,
));
}
None
}
pub fn with_visible_callable_arities<'a>(
&'a self,
analyzer: &CppGraphSource<'_>,
cpp: &dyn CppSource,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
) -> Cow<'a, Self> {
let macro_parameter_arity =
visibility.callable_parameter_macro_arity(&self.target, self.param_types.as_deref());
let activated_callable_arities =
visibility.callable_arities_for_target(analyzer, cpp, file, prepared, self);
if macro_parameter_arity.is_none() && activated_callable_arities.is_empty() {
return Cow::Borrowed(self);
}
let mut effective = self.clone();
if let Some(macro_parameter_arity) = macro_parameter_arity {
effective.callable_arity = Some(macro_parameter_arity);
}
effective.activated_callable_arities = activated_callable_arities;
Cow::Owned(effective)
}
pub fn callable_arity_at(&self, byte: usize) -> Option<CallableArity> {
let base = self.callable_arity?;
Some(
self.activated_callable_arities
.iter()
.filter(|candidate| candidate.activation_byte <= byte)
.fold(base, |arity, candidate| {
merge_compatible_callable_arities(arity, candidate.arity).unwrap_or(arity)
}),
)
}
pub fn new(
target: CodeUnit,
kind: TargetKind,
owner: Option<CodeUnit>,
member_name: String,
callable_arity: Option<CallableArity>,
param_types: Option<Vec<String>>,
) -> Self {
Self {
target,
kind,
owner,
member_name,
callable_arity,
activated_callable_arities: Vec::new(),
param_types,
enum_owner_kind: EnumOwnerKind::NonEnum,
owner_is_forward_declaration: false,
callable_has_definition_body: false,
}
}
}
fn callable_target_has_definition_body(analyzer: &CppGraphSource<'_>, target: &CodeUnit) -> bool {
analyzer
.signature_metadata(target)
.iter()
.any(|metadata| !metadata.is_declaration_only())
}
fn logical_symbol_key(unit: &CodeUnit) -> LogicalSymbolKey {
LogicalSymbolKey {
kind: unit.kind(),
fq_name: unit.fq_name(),
signature: unit.signature().map(str::to_string),
}
}
fn classify_enum_owner(analyzer: &CppGraphSource<'_>, owner: &CodeUnit) -> EnumOwnerKind {
let Some(facts) = analyzer.declaration_source_properties(owner) else {
return EnumOwnerKind::NonEnum;
};
let Some(first) = facts.first() else {
return EnumOwnerKind::NonEnum;
};
if facts.iter().all(|fact| fact.enum_kind == first.enum_kind) {
first.enum_kind
} else {
EnumOwnerKind::NonEnum
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct CppScanBinding {
pub unit: Option<CodeUnit>,
pub type_name: Option<String>,
pub indirection: i32,
}
impl CppScanBinding {
pub fn from_unit(unit: CodeUnit, indirection: i32) -> Self {
Self::from_type_name(cpp_name_for(&unit), Some(unit), indirection)
}
pub fn from_type_name(type_name: String, unit: Option<CodeUnit>, indirection: i32) -> Self {
Self {
type_name: Some(type_name),
unit,
indirection,
}
}
pub fn as_arg_type(&self) -> Option<CppArgType> {
let name = self
.type_name
.clone()
.or_else(|| self.unit.as_ref().map(cpp_name_for))?;
Some(CppArgType {
name,
unit: self.unit.clone(),
indirection: self.indirection,
pointee_const: false,
})
}
}
type VisibilityEntry<T> = Arc<ReplayingMemoEntry<T, OpaqueReadReceipt>>;
type VisibilityMemo<K, V> =
ReplayingMemo<KeyedPoolSafeMemo<K, ReplayingMemoEntry<V, OpaqueReadReceipt>>>;
type BoundedVisibilityMemo<K, V> = ReplayingMemo<Mutex<HashMap<K, VisibilityEntry<V>>>>;
type IncludeProtectionMemo = ReplayingMemo<
Mutex<
HashMap<
ProjectFile,
Arc<PoolSafeMemo<ReplayingMemoEntry<MacroIncludeProtection, OpaqueReadReceipt>>>,
>,
>,
>;
type VisibilityOnce<T> = ReplayingMemo<OnceLock<VisibilityEntry<T>>>;
fn try_visibility_memo_value<K: Eq + Hash + Clone, V: Clone, E>(
memo: &VisibilityMemo<K, V>,
reads: &ReadCapability,
key: K,
build: impl FnOnce() -> Result<V, E>,
) -> Result<V, E> {
let value = memo.read(&reads.memo_attribution(), |entries, capture| {
entries
.cell(&key)
.get_or_try_build_pool_independent(|| capture.try_build(build))
})?;
Ok((*value).clone())
}
pub(crate) fn visibility_memo_value<K: Eq + Hash + Clone, V: Clone>(
memo: &VisibilityMemo<K, V>,
reads: &ReadCapability,
key: K,
build: impl FnOnce() -> V,
) -> V {
try_visibility_memo_value(memo, reads, key, || {
Ok::<_, std::convert::Infallible>(build())
})
.expect("infallible C++ visibility build")
}
fn bounded_visibility_memo_value<K: Eq + Hash + Clone, V: Clone>(
memo: &BoundedVisibilityMemo<K, V>,
reads: &ReadCapability,
key: &K,
capacity: usize,
build: impl FnOnce() -> V,
) -> V {
let value = memo
.read(&reads.memo_attribution(), |entries, capture| {
if let Some(value) = entries
.lock()
.expect("C++ bounded visibility memo poisoned")
.get(key)
.cloned()
{
return Ok::<_, std::convert::Infallible>(value);
}
let value = Arc::new(capture.build(build));
let mut entries = entries
.lock()
.expect("C++ bounded visibility memo poisoned");
if let Some(existing) = entries.get(key).cloned() {
return Ok(existing);
}
if entries.len() < capacity {
entries.insert(key.clone(), Arc::clone(&value));
}
Ok(value)
})
.expect("infallible bounded C++ visibility build");
(*value).clone()
}
fn visibility_once_value<T: Clone>(
memo: &VisibilityOnce<T>,
reads: &ReadCapability,
build: impl FnOnce() -> T,
) -> T {
let value = memo
.read(&reads.memo_attribution(), |cell, capture| {
Ok::<_, std::convert::Infallible>(Arc::clone(
cell.get_or_init(|| Arc::new(capture.build(build))),
))
})
.expect("infallible C++ visibility cell build");
(*value).clone()
}
pub struct VisibilityCell<T> {
memo: VisibilityOnce<T>,
reads: ReadCapability,
}
impl<T: Clone> VisibilityCell<T> {
fn new(reads: ReadCapability) -> Self {
Self {
memo: VisibilityOnce::default(),
reads,
}
}
pub fn get_or_init(&self, build: impl FnOnce() -> T) -> T {
visibility_once_value(&self.memo, &self.reads, build)
}
pub fn get(&self) -> Option<T> {
self.memo
.read(&self.reads.memo_attribution(), |cell, _| {
cell.get().cloned().ok_or(())
})
.ok()
.map(|value| (*value).clone())
}
}
type AliasCell = Arc<OnceLock<Box<[CppAlias]>>>;
pub type OrdinaryTypeImportCell = Arc<EffectiveUsingIndex>;
pub type MacroEventCell = Arc<VisibilityCell<Arc<[MacroEvent]>>>;
type MacroEnvironmentCheckpointCell = Arc<VisibilityCell<Arc<MacroEnvironmentCheckpoints>>>;
struct StructuredIncludeFact {
start_byte: usize,
end_byte: usize,
path: String,
}
#[derive(Clone, Default)]
pub struct MacroEnvironment {
bindings: HashMap<String, MacroBinding>,
known_undefined_names: HashSet<String>,
build_proven_defines: HashSet<String>,
unknown_names: bool,
applied_pragma_once_files: HashSet<ProjectFile>,
maybe_applied_pragma_once_files: HashSet<ProjectFile>,
}
pub const MACRO_ENVIRONMENT_CHECKPOINT_STRIDE: usize = 32;
struct MacroEnvironmentCheckpoint {
frontier: usize,
environment: Arc<MacroEnvironment>,
}
struct MacroEnvironmentCheckpoints {
checkpoints: Vec<MacroEnvironmentCheckpoint>,
}
impl MacroEnvironmentCheckpoints {
fn at_or_before(&self, frontier: usize) -> &MacroEnvironmentCheckpoint {
let index = self
.checkpoints
.partition_point(|checkpoint| checkpoint.frontier <= frontier);
assert!(
index > 0,
"a checkpoint vector starts at frontier zero, which precedes every request"
);
&self.checkpoints[index - 1]
}
}
impl MacroEnvironment {
fn binding(&self, name: &str) -> Option<&MacroBinding> {
self.bindings.get(name)
}
fn may_bind(&self, name: &str) -> bool {
self.bindings.contains_key(name) || self.unknown_names
}
fn insert(&mut self, name: String, binding: MacroBinding) {
self.known_undefined_names.remove(&name);
self.bindings.insert(name, binding);
}
fn remove(&mut self, name: &str) {
self.bindings.remove(name);
self.known_undefined_names.insert(name.to_string());
}
fn remove_known_undefined(&mut self, name: &str) {
self.known_undefined_names.remove(name);
}
fn mark_unknown_names(&mut self, source: &ProjectFile, byte: usize) {
for binding in self.bindings.values_mut() {
*binding = MacroBinding::uncertain_from(binding, source, byte);
}
self.known_undefined_names.clear();
self.build_proven_defines.clear();
self.unknown_names = true;
}
fn guard_requirements_may_hold(&self, guards: &HashSet<PreprocessorGuard>) -> bool {
guards.iter().all(|guard| self.guard_may_hold(guard))
}
fn guard_may_hold(&self, guard: &PreprocessorGuard) -> bool {
self.boolean_guard_may_hold(&guard.as_boolean_expression())
}
fn boolean_guard_may_hold(&self, expression: &BooleanGuardExpression) -> bool {
match expression {
BooleanGuardExpression::Defined(name) => !self.known_undefined_names.contains(name),
BooleanGuardExpression::Undefined(name) => {
self.bindings
.get(name)
.is_none_or(|binding| !binding.is_exact())
&& (!self.build_proven_defines.contains(name)
|| self.known_undefined_names.contains(name))
}
BooleanGuardExpression::Truthy(_) | BooleanGuardExpression::Falsy(_) => true,
BooleanGuardExpression::Opaque(_)
| BooleanGuardExpression::NegatedOpaque(_)
| BooleanGuardExpression::Constant(true) => true,
BooleanGuardExpression::Constant(false) => false,
BooleanGuardExpression::All(expressions) => expressions
.iter()
.all(|expression| self.boolean_guard_may_hold(expression)),
BooleanGuardExpression::Any(expressions) => expressions
.iter()
.any(|expression| self.boolean_guard_may_hold(expression)),
}
}
}
fn any_visible_source_file_matches(
visible_source_files_by_root: &HashMap<ProjectFile, HashSet<ProjectFile>>,
file: &ProjectFile,
mut predicate: impl FnMut(&ProjectFile) -> bool,
) -> bool {
match visible_source_files_by_root.get(file) {
Some(files) => files.iter().any(predicate),
None => predicate(file),
}
}
#[derive(Clone)]
pub enum EffectiveUsingTarget {
Ordinary {
name: String,
target_components: Vec<String>,
global: bool,
},
Namespace {
namespace_components: Vec<String>,
global: bool,
},
}
#[derive(Clone)]
pub struct OrdinaryTypeImport {
pub target: EffectiveUsingTarget,
pub source: ProjectFile,
pub declaration_byte: usize,
pub scope_start: usize,
pub scope_end: usize,
pub scope_depth: usize,
pub block_scope: bool,
pub lexical_depth: usize,
pub declaration_namespace: Vec<String>,
pub namespace_scope: Option<Vec<String>>,
pub resolved_target_components: Option<Vec<String>>,
pub required_guards: HashSet<PreprocessorGuard>,
}
#[derive(Clone)]
pub struct ConditionalIncludeProjection {
pub activation_byte: usize,
pub required_guards: HashSet<PreprocessorGuard>,
pub partial_guards: HashSet<PreprocessorGuard>,
}
#[derive(Default)]
pub struct SourceUsingIndex {
pub ordinary_by_name: HashMap<String, Vec<OrdinaryTypeImport>>,
pub directives: Vec<OrdinaryTypeImport>,
}
#[derive(Default)]
pub struct ProjectUsingIndex {
pub ordinary_by_name: HashMap<String, Vec<OrdinaryTypeImport>>,
pub directives: Vec<OrdinaryTypeImport>,
}
type EffectiveUsingProjectionCell = Arc<VisibilityCell<Arc<[OrdinaryTypeImport]>>>;
pub struct EffectiveUsingIndex {
reads: ReadCapability,
projected_by_name: VisibilityMemo<String, EffectiveUsingProjectionCell>,
}
impl EffectiveUsingIndex {
fn new(_root: ProjectFile, reads: ReadCapability) -> Self {
Self {
reads,
projected_by_name: VisibilityMemo::default(),
}
}
pub fn projection_cell(&self, name: &str) -> EffectiveUsingProjectionCell {
visibility_memo_value(
&self.projected_by_name,
&self.reads,
name.to_owned(),
|| Arc::new(VisibilityCell::new(self.reads.clone())),
)
}
}
pub enum OrdinaryTypeImportResolution {
Resolved {
target: CodeUnit,
target_components: Vec<String>,
lexical_depth: usize,
is_direct: bool,
},
Ambiguous {
lexical_depth: usize,
},
Missing,
}
type ConditionalIncludeProjectionIndex = HashMap<ProjectFile, Arc<[ConditionalIncludeProjection]>>;
struct ExtractedComparable {
shapes: Vec<CppComparableSlot>,
suffix: String,
template: Option<CppTemplateContext>,
}
const MAX_COMPARABLE_ALIAS_HOPS: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum IncludePathAdmission {
Proven,
Compatible,
}
impl IncludePathAdmission {
fn admits(
self,
required: &HashSet<PreprocessorGuard>,
partial: &HashSet<PreprocessorGuard>,
reference_guards: Option<&HashSet<PreprocessorGuard>>,
) -> bool {
match self {
Self::Proven => guard_requirements_hold_at_reference(required, reference_guards),
Self::Compatible => {
guard_requirements_hold_at_reference(partial, reference_guards)
&& guards_compatible_at_reference(required, reference_guards)
}
}
}
}
pub struct VisibilityIndex<'a> {
cpp: &'a dyn CppSource,
token: QueryToken<'a>,
data: Arc<VisibilityIndexData>,
}
impl std::ops::Deref for VisibilityIndex<'_> {
type Target = VisibilityIndexData;
fn deref(&self) -> &Self::Target {
&self.data
}
}
pub struct VisibilityIndexData {
pub(crate) reads: ReadCapability,
pub visible_by_file: HashMap<ProjectFile, HashSet<CodeUnit>>,
visible_by_identifier: HashMap<ProjectFile, HashMap<String, Vec<CodeUnit>>>,
global_field_internal_linkage: HashMap<CodeUnit, bool>,
visible_source_files_by_root: HashMap<ProjectFile, HashSet<ProjectFile>>,
alias_cells: VisibilityMemo<ProjectFile, AliasCell>,
visible_parser_alias_name_sets: VisibilityMemo<ProjectFile, Arc<HashSet<String>>>,
parser_alias_target_matches: VisibilityMemo<(ProjectFile, String, LogicalSymbolKey), bool>,
ordinary_type_import_cells: VisibilityMemo<ProjectFile, OrdinaryTypeImportCell>,
project_using_index: VisibilityOnce<Arc<ProjectUsingIndex>>,
namespace_alias_sites: VisibilityMemo<ProjectFile, super::namespace_alias::NamespaceAliasCell>,
callable_reference_specs:
VisibilityMemo<(ProjectFile, LogicalSymbolKey), Option<Arc<TargetSpec>>>,
structured_include_fact_cells: VisibilityMemo<ProjectFile, Arc<[StructuredIncludeFact]>>,
include_activation_cells: VisibilityMemo<(ProjectFile, ProjectFile), Option<usize>>,
compile_proven_guard_cells: VisibilityMemo<ProjectFile, Arc<HashSet<PreprocessorGuard>>>,
include_path_admission_cells: VisibilityMemo<ProjectFile, IncludePathAdmission>,
conditional_include_projection_cells:
VisibilityMemo<ProjectFile, Arc<ConditionalIncludeProjectionIndex>>,
#[cfg(any(test, feature = "test-support"))]
conditional_include_projection_index_build_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
conditional_include_projection_state_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
conditional_include_target_state_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
include_activation_build_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
using_donor_activation_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
using_namespace_lookup_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
using_name_candidate_inspection_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
callable_reference_spec_build_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
alias_source_parse_counts: Mutex<HashMap<ProjectFile, usize>>,
#[cfg(any(test, feature = "test-support"))]
visible_parser_alias_name_set_build_count: AtomicUsize,
parser_alias_fallback_calls: AtomicUsize,
parser_alias_fallback_files: AtomicUsize,
parser_alias_source_parses: AtomicUsize,
parser_alias_fallback_elapsed_micros: AtomicUsize,
field_type_facts: VisibilityMemo<CodeUnit, Option<DeclaredFieldTypeFact>>,
structured_alias_targets: VisibilityMemo<CodeUnit, Option<StructuredAliasTarget>>,
callable_comparables: VisibilityMemo<CodeUnit, Option<Arc<ExtractedComparable>>>,
class_definition_agreements: VisibilityMemo<(CodeUnit, CodeUnit), bool>,
callable_metadata: BoundedVisibilityMemo<CodeUnit, Arc<Vec<SignatureMetadata>>>,
comparable_name_declarations: VisibilityMemo<StructuredTypeName, Option<CodeUnit>>,
indexed_structural_class_scopes:
VisibilityMemo<(ProjectFile, usize, usize), Option<Vec<String>>>,
indexed_enclosing_owner_scopes:
VisibilityMemo<(ProjectFile, usize, usize), Option<Vec<String>>>,
precise_parent_cache: VisibilityMemo<CodeUnit, Option<CodeUnit>>,
c_tag_kind_cache: VisibilityMemo<CodeUnit, Option<CppCTagKind>>,
c_tag_complete_definition_cache: VisibilityMemo<CodeUnit, Option<CodeUnit>>,
macro_event_cells: VisibilityMemo<ProjectFile, MacroEventCell>,
macro_event_name_sets: VisibilityMemo<ProjectFile, Arc<HashSet<String>>>,
pub macro_include_protection_cells: IncludeProtectionMemo,
macro_environment_checkpoints: VisibilityMemo<ProjectFile, MacroEnvironmentCheckpointCell>,
macro_replacements: VisibilityMemo<(ProjectFile, usize), Arc<ParsedMacroReplacement>>,
macro_local_binding_templates:
VisibilityMemo<(ProjectFile, usize), Option<Arc<MacroLocalBindingTemplate>>>,
pub(crate) macro_lexical_templates:
VisibilityMemo<(ProjectFile, usize), Option<crate::graph::macro_lexical::MacroTemplate>>,
macro_lexical_indexes:
BoundedVisibilityMemo<ProjectFile, Arc<crate::graph::macro_lexical::MacroLexicalIndex>>,
macro_replacement_bodies:
VisibilityMemo<(ProjectFile, usize), Option<Arc<ParsedReplacementBody>>>,
macro_type_parameters: VisibilityMemo<(ProjectFile, usize), Option<Arc<[usize]>>>,
callable_parameter_macro_arities: VisibilityMemo<(ProjectFile, String), Option<CallableArity>>,
#[cfg(any(test, feature = "test-support"))]
pub macro_replacement_parse_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
pub macro_event_application_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
pub macro_environment_checkpoint_build_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
pub macro_environment_copy_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
pub macro_environment_request_count: AtomicUsize,
cpp_template_metadata: HashMap<CodeUnit, CppTemplateMetadata>,
cpp_template_families: HashMap<String, Vec<CodeUnit>>,
#[cfg(any(test, feature = "test-support"))]
qualified_candidate_inspections: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
target_preserving_type_resolution_count: AtomicUsize,
#[cfg(any(test, feature = "test-support"))]
visibility_identifier_lookup_count: usize,
#[cfg(any(test, feature = "test-support"))]
visibility_identifier_batch_count: usize,
}
impl Drop for VisibilityIndexData {
fn drop(&mut self) {
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_none() {
return;
}
#[cfg(any(test, feature = "test-support"))]
eprintln!(
"BIFROST_CPP_MACRO_STATS requests={} copies={} checkpoint_builds={} applications={}",
self.macro_environment_request_count.load(Ordering::Relaxed),
self.macro_environment_copy_count.load(Ordering::Relaxed),
self.macro_environment_checkpoint_build_count
.load(Ordering::Relaxed),
self.macro_event_application_count.load(Ordering::Relaxed),
);
let calls = self.parser_alias_fallback_calls.load(Ordering::Relaxed);
if calls == 0 {
return;
}
eprintln!(
"BIFROST_CPP_ALIAS_FALLBACK_STATS calls={} files={} source_parses={} elapsed_ms={}",
calls,
self.parser_alias_fallback_files.load(Ordering::Relaxed),
self.parser_alias_source_parses.load(Ordering::Relaxed),
self.parser_alias_fallback_elapsed_micros
.load(Ordering::Relaxed)
/ 1_000,
);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PreprocessorGuard {
Defined(String),
Undefined(String),
Boolean(BooleanGuardExpression),
Expression(String),
NegatedExpression(String),
Constant(bool),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum BooleanGuardExpression {
Defined(String),
Undefined(String),
Truthy(String),
Falsy(String),
Opaque(String),
NegatedOpaque(String),
All(Vec<BooleanGuardExpression>),
Any(Vec<BooleanGuardExpression>),
Constant(bool),
}
impl BooleanGuardExpression {
fn negated(&self) -> Self {
match self {
Self::Defined(name) => Self::Undefined(name.clone()),
Self::Undefined(name) => Self::Defined(name.clone()),
Self::Truthy(name) => Self::Falsy(name.clone()),
Self::Falsy(name) => Self::Truthy(name.clone()),
Self::Opaque(expression) => Self::NegatedOpaque(expression.clone()),
Self::NegatedOpaque(expression) => Self::Opaque(expression.clone()),
Self::All(expressions) => Self::any(expressions.iter().map(Self::negated)),
Self::Any(expressions) => Self::all(expressions.iter().map(Self::negated)),
Self::Constant(value) => Self::Constant(!value),
}
}
fn all(expressions: impl IntoIterator<Item = Self>) -> Self {
Self::normalized(expressions, true)
}
fn any(expressions: impl IntoIterator<Item = Self>) -> Self {
Self::normalized(expressions, false)
}
fn normalized(expressions: impl IntoIterator<Item = Self>, conjunction: bool) -> Self {
let mut normalized = Vec::new();
for expression in expressions {
match expression {
Self::All(nested) if conjunction => normalized.extend(nested),
Self::Any(nested) if !conjunction => normalized.extend(nested),
Self::Constant(value) if value == conjunction => {}
Self::Constant(value) => return Self::Constant(value),
expression => normalized.push(expression),
}
}
normalized.sort_unstable();
normalized.dedup();
match normalized.len() {
0 => Self::Constant(conjunction),
1 => normalized.pop().expect("one Boolean guard expression"),
_ if conjunction => Self::All(normalized),
_ => Self::Any(normalized),
}
}
fn implies(&self, required: &Self) -> bool {
if self == required
|| matches!(self, Self::Constant(false))
|| matches!(required, Self::Constant(true))
{
return true;
}
if matches!(
(self, required),
(Self::Truthy(active), Self::Defined(required))
| (Self::Undefined(active), Self::Falsy(required))
if active == required
) {
return true;
}
match self {
Self::Any(active) => active.iter().all(|expression| expression.implies(required)),
Self::All(active) => match required {
Self::All(required) => required.iter().all(|expression| self.implies(expression)),
_ => active.iter().any(|expression| expression.implies(required)),
},
_ => match required {
Self::Any(required) => required.iter().any(|expression| self.implies(expression)),
Self::All(required) => required.iter().all(|expression| self.implies(expression)),
_ => false,
},
}
}
fn may_depend_on_macro(&self, macro_name: &str) -> bool {
match self {
Self::Defined(name)
| Self::Undefined(name)
| Self::Truthy(name)
| Self::Falsy(name) => name == macro_name,
Self::Opaque(_) | Self::NegatedOpaque(_) => true,
Self::All(expressions) | Self::Any(expressions) => expressions
.iter()
.any(|expression| expression.may_depend_on_macro(macro_name)),
Self::Constant(_) => false,
}
}
pub fn heap_size(&self) -> usize {
match self {
Self::Defined(value)
| Self::Undefined(value)
| Self::Truthy(value)
| Self::Falsy(value)
| Self::Opaque(value)
| Self::NegatedOpaque(value) => value.len(),
Self::All(expressions) | Self::Any(expressions) => {
expressions
.iter()
.fold(std::mem::size_of::<Vec<Self>>(), |size, expression| {
size.saturating_add(std::mem::size_of::<Self>())
.saturating_add(expression.heap_size())
})
}
Self::Constant(_) => 0,
}
}
}
impl PreprocessorGuard {
fn as_boolean_expression(&self) -> BooleanGuardExpression {
match self {
Self::Defined(name) => BooleanGuardExpression::Defined(name.clone()),
Self::Undefined(name) => BooleanGuardExpression::Undefined(name.clone()),
Self::Boolean(expression) => expression.clone(),
Self::Constant(value) => BooleanGuardExpression::Constant(*value),
Self::Expression(expression) => BooleanGuardExpression::Opaque(expression.clone()),
Self::NegatedExpression(expression) => {
BooleanGuardExpression::NegatedOpaque(expression.clone())
}
}
}
fn negated(&self) -> Self {
match self {
Self::Defined(name) => Self::Undefined(name.clone()),
Self::Undefined(name) => Self::Defined(name.clone()),
Self::Boolean(expression) => Self::Boolean(expression.negated()),
Self::Expression(expression) => Self::NegatedExpression(expression.clone()),
Self::NegatedExpression(expression) => Self::Expression(expression.clone()),
Self::Constant(value) => Self::Constant(!value),
}
}
fn may_depend_on_macro(&self, macro_name: &str) -> bool {
match self {
Self::Defined(name) | Self::Undefined(name) => name == macro_name,
Self::Boolean(expression) => expression.may_depend_on_macro(macro_name),
Self::Expression(_) | Self::NegatedExpression(_) => true,
Self::Constant(_) => false,
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum MacroDefinition {
Object {
replacement: String,
},
Function {
parameters: Vec<String>,
replacement: String,
},
VariadicFunction {
parameters: Vec<String>,
replacement: String,
},
Unsupported,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MacroIncludeProtection {
MacroGuard(String),
PragmaOnce,
None,
}
enum ParsedMacroReplacement {
Parsed { source: String, tree: Tree },
Unsupported,
}
const MACRO_BODY_SENTINEL_PREFIX: &str = "void __bifrost_macro_body() { ";
const MACRO_BODY_SENTINEL_SUFFIX: &str = "; }";
type MacroOriginMap = Box<[std::ops::Range<usize>]>;
pub struct ParsedReplacementBody {
pub source: String,
pub tree: Tree,
pub body_offset: usize,
pub parameters: Vec<String>,
original_offsets: MacroOriginMap,
}
impl ParsedReplacementBody {
pub fn statements(&self) -> Option<Node<'_>> {
first_descendant_of_kind(self.tree.root_node(), "function_definition")?
.child_by_field_name("body")
}
pub fn file_range(&self, node: Node<'_>, replacement_start: usize) -> std::ops::Range<usize> {
assert!(
node.start_byte() >= self.body_offset,
"synthetic sentinel node cannot be mapped to a macro replacement"
);
assert!(
node.end_byte() >= self.body_offset,
"synthetic sentinel node cannot be mapped to a macro replacement"
);
let start_offset = node.start_byte() - self.body_offset;
let end_offset = node.end_byte() - self.body_offset;
assert!(start_offset <= end_offset);
let start_origin = self
.original_offsets
.get(start_offset)
.expect("replacement node start must have a source mapping")
.start;
let end_origin = if start_offset == end_offset {
start_origin
} else {
self.original_offsets
.get(end_offset - 1)
.expect("replacement node end must have a source mapping")
.end
};
assert!(start_origin <= end_origin);
let start = replacement_start + start_origin;
let end = replacement_start + end_origin;
start..end
}
fn expands_variadic_arguments(&self) -> bool {
let mut stack = vec![self.tree.root_node()];
while let Some(node) = stack.pop() {
if matches!(
node.kind(),
"identifier" | "type_identifier" | "field_identifier" | "namespace_identifier"
) && node_text(node, &self.source) == "__VA_ARGS__"
{
return true;
}
push_named_children_reversed(node, &mut stack);
}
false
}
}
pub fn parse_cpp_integer_literal(text: &str) -> Option<i128> {
let compact = text.chars().filter(|ch| *ch != '\'').collect::<String>();
let (radix, digits_start, digit_matches): (u32, usize, fn(char) -> bool) =
if compact.starts_with("0x") || compact.starts_with("0X") {
(16, 2, |ch| ch.is_ascii_hexdigit())
} else if compact.starts_with("0b") || compact.starts_with("0B") {
(2, 2, |ch| matches!(ch, '0' | '1'))
} else if compact.starts_with('0') && compact.len() > 1 {
(8, 0, |ch| matches!(ch, '0'..='7'))
} else {
(10, 0, |ch| ch.is_ascii_digit())
};
let digit_len = compact[digits_start..]
.chars()
.take_while(|ch| digit_matches(*ch))
.map(char::len_utf8)
.sum::<usize>();
if digit_len == 0 {
return None;
}
let digits_end = digits_start + digit_len;
if !compact[digits_end..]
.chars()
.all(|ch| matches!(ch, 'u' | 'U' | 'l' | 'L' | 'z' | 'Z'))
{
return None;
}
i128::from_str_radix(&compact[digits_start..digits_end], radix).ok()
}
#[derive(Clone)]
enum MacroLocalBindingTypeTemplate {
Parameter(usize),
Fixed(String),
}
#[derive(Clone)]
struct MacroLocalBindingTemplate {
name: String,
declared_type: MacroLocalBindingTypeTemplate,
pointer_depth: i32,
}
pub struct MacroLocalBinding<'tree> {
pub name: String,
pub type_name: String,
pub type_node: Option<Node<'tree>>,
pub pointer_depth: i32,
pub proven_unit: Option<CodeUnit>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MacroLexicalBindingKind {
Parameter,
Local,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MacroLexicalBinding {
pub definition: ProjectFile,
pub kind: MacroLexicalBindingKind,
pub name: String,
pub name_range: std::ops::Range<usize>,
pub declaration_range: std::ops::Range<usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MacroLexicalReferences {
pub references: Vec<(std::ops::Range<usize>, MacroLexicalBinding)>,
pub truncated: bool,
pub cancelled: bool,
}
fn macro_replacement_type_parameters(
body: &ParsedReplacementBody,
parameters: &[String],
) -> Option<Vec<usize>> {
let mut found = Vec::new();
let mut stack = vec![body.tree.root_node()];
while let Some(node) = stack.pop() {
let type_position = node.kind() == "type_identifier"
|| (node.kind() == "identifier"
&& node.parent().is_some_and(|parent| {
parent.kind() == "type_descriptor"
&& parent.child_by_field_name("type") == Some(node)
}));
let offsetof_type_position = node.kind() == "identifier"
&& node
.parent()
.filter(|parent| parent.kind() == "argument_list")
.and_then(|arguments| arguments.parent())
.is_some_and(|call| {
call.kind() == "call_expression"
&& call
.child_by_field_name("function")
.is_some_and(|function| {
function.kind() == "identifier"
&& node_text(function, &body.source) == "offsetof"
})
&& call
.child_by_field_name("arguments")
.is_some_and(|arguments| {
argument_children(arguments).next() == Some(node)
})
});
if (type_position || offsetof_type_position)
&& let Some(index) = parameters
.iter()
.position(|parameter| parameter == node_text(node, &body.source))
&& !found.contains(&index)
{
found.push(index);
}
push_named_children_reversed(node, &mut stack);
}
(!found.is_empty()).then_some(found)
}
fn macro_replacement_type_parameter(
body: &ParsedReplacementBody,
parameters: &[String],
) -> Option<usize> {
let mut parameters = macro_replacement_type_parameters(body, parameters)?;
(parameters.len() == 1).then(|| parameters.pop().unwrap())
}
pub(crate) fn function_macro_invocation_parts(node: Node<'_>) -> Option<(Node<'_>, Vec<Node<'_>>)> {
let (name, arguments) = match node.kind() {
"call_expression" => (
node.child_by_field_name("function")?,
argument_children(node.child_by_field_name("arguments")?).collect(),
),
"macro_type_specifier" => (
node.child_by_field_name("name")?,
vec![node.child_by_field_name("type")?],
),
_ => return None,
};
(name.kind() == "identifier").then_some((name, arguments))
}
pub(crate) fn macro_type_argument_node<'tree>(
node: Node<'tree>,
source: &str,
) -> Option<Node<'tree>> {
match node.kind() {
"type_descriptor" => {
let type_child = node
.child_by_field_name("type")
.or_else(|| first_type_child(node))?;
for index in (0..node.named_child_count()).rev() {
let child = node.named_child(index)?;
if child != type_child
&& matches!(
child.kind(),
"identifier"
| "type_identifier"
| "qualified_identifier"
| "scoped_type_identifier"
)
{
return Some(child);
}
}
macro_type_argument_node(type_child, source)
}
"class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier" => {
node.child_by_field_name("name")
}
"identifier" if matches!(node_text(node, source), "struct" | "union") => node
.next_named_sibling()
.filter(|sibling| sibling.is_error() && sibling.named_child_count() == 1)
.and_then(|error| error.named_child(0))
.filter(|name| matches!(name.kind(), "identifier" | "type_identifier")),
_ => cpp_name_component_nodes(node).is_some().then_some(node),
}
}
fn c_function_macro_argument<'tree>(
node: Node<'tree>,
) -> Option<(Node<'tree>, usize, Node<'tree>)> {
let mut current = node;
while let Some(parent) = current.parent() {
if parent.kind() == "argument_list" {
let call = parent.parent().filter(|call| {
call.kind() == "call_expression"
&& call.child_by_field_name("arguments") == Some(parent)
&& call
.child_by_field_name("function")
.is_some_and(|function| {
function.kind() == "identifier"
&& !node_range_contains(function, current)
})
})?;
let mut actuals = argument_children(parent).enumerate();
let (index, argument) = actuals.find(|(_, argument)| {
node_range_contains(*argument, node)
&& (argument.start_byte() == current.start_byte()
|| argument.end_byte() == current.end_byte())
})?;
return Some((call, index, argument));
}
current = parent;
}
None
}
fn recognized_c_macro_declarator_binding<'tree>(
statement: Node<'tree>,
source: &str,
) -> Option<MacroLocalBinding<'tree>> {
if statement.kind() == "declaration" {
let specifier = statement
.child_by_field_name("type")
.filter(|type_node| type_node.kind() == "macro_type_specifier")?;
let macro_name = specifier
.child_by_field_name("name")
.filter(|name| name.kind() == "identifier")?;
if node_text(macro_name, source) != "g_autoptr" {
return None;
}
let type_node = specifier
.child_by_field_name("type")
.and_then(|descriptor| descriptor.child_by_field_name("type"))?;
let declarator = statement
.child_by_field_name("declarator")
.filter(|declarator| declarator.kind() == "init_declarator")?;
let name_node = declarator
.child_by_field_name("declarator")
.filter(|name| name.kind() == "identifier")?;
declarator.child_by_field_name("value")?;
let name = node_text(name_node, source).trim();
let type_name = node_text(type_node, source).trim();
if name.is_empty() || type_name.is_empty() {
return None;
}
return Some(MacroLocalBinding {
name: name.to_string(),
type_name: type_name.to_string(),
type_node: Some(type_node),
pointer_depth: 1,
proven_unit: None,
});
}
let assignment = match statement.kind() {
"assignment_expression" => statement,
"expression_statement" => {
single_named_child_matching(statement, |child| !child.is_extra())?
}
_ => return None,
};
if assignment.kind() != "assignment_expression" {
return None;
}
let call = assignment.child_by_field_name("left")?;
if call.kind() != "call_expression" {
return None;
}
let function = call.child_by_field_name("function")?;
if function.kind() != "identifier" || node_text(function, source) != "g_autoptr" {
return None;
}
let arguments = call.child_by_field_name("arguments")?;
let mut actuals = argument_children(arguments);
let type_node = actuals.next()?;
if actuals.next().is_some()
|| !matches!(
type_node.kind(),
"identifier"
| "type_identifier"
| "qualified_identifier"
| "scoped_type_identifier"
| "template_type"
)
{
return None;
}
let name_node = (0..assignment.named_child_count())
.filter_map(|index| assignment.named_child(index))
.filter(|child| child.kind() == "ERROR")
.filter_map(|error| {
(error.named_child_count() == 1)
.then(|| error.named_child(0))
.flatten()
})
.find(|node| node.kind() == "identifier")?;
let name = node_text(name_node, source).trim();
let type_name = node_text(type_node, source).trim();
if name.is_empty() || type_name.is_empty() {
return None;
}
Some(MacroLocalBinding {
name: name.to_string(),
type_name: type_name.to_string(),
type_node: Some(type_node),
pointer_depth: 1,
proven_unit: None,
})
}
#[derive(Clone, PartialEq, Eq)]
pub struct MacroBinding {
source: ProjectFile,
declaration_byte: usize,
definition: MacroDefinition,
exact: bool,
}
impl MacroBinding {
fn ambiguous(source: &ProjectFile, declaration_byte: usize) -> Self {
Self {
source: source.clone(),
declaration_byte,
definition: MacroDefinition::Unsupported,
exact: false,
}
}
fn is_exact(&self) -> bool {
self.exact
}
fn uncertain_from(current: &Self, source: &ProjectFile, declaration_byte: usize) -> Self {
Self {
source: source.clone(),
declaration_byte,
definition: current.definition.clone(),
exact: false,
}
}
}
type OwningPreprocessorConditionals = Box<[usize]>;
#[derive(Clone)]
pub enum MacroEvent {
Define {
name: String,
binding: MacroBinding,
byte: usize,
conditionals: OwningPreprocessorConditionals,
},
Undef {
name: String,
byte: usize,
conditionals: OwningPreprocessorConditionals,
},
Include {
targets: Vec<ProjectFile>,
byte: usize,
conditionals: OwningPreprocessorConditionals,
},
Invalidate {
byte: usize,
},
}
impl MacroEvent {
pub fn byte(&self) -> usize {
match self {
Self::Define { byte, .. }
| Self::Undef { byte, .. }
| Self::Include { byte, .. }
| Self::Invalidate { byte } => *byte,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CallArityEvidence {
Exact(usize),
Unknown,
}
impl CallArityEvidence {
pub fn exact(self) -> Option<usize> {
match self {
Self::Exact(arity) => Some(arity),
Self::Unknown => None,
}
}
pub fn accepts(self, expected: CallableArity) -> Option<bool> {
self.exact().map(|arity| expected.accepts(arity))
}
}
struct CppAlias {
name: String,
target: String,
namespace: Option<String>,
}
type ReceiverResolver<'a> = dyn for<'tree> Fn(Node<'tree>, &str) -> Vec<CodeUnit> + 'a;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CppTemplateResolutionError {
AliasCycle { alias: CodeUnit },
ArgumentBinding,
Substitution,
PrimarySelection,
AmbiguousSpecialization { candidates: Vec<CodeUnit> },
}
fn distinct_visible_symbols<'u>(units: impl Iterator<Item = &'u CodeUnit>) -> Vec<CodeUnit> {
let mut distinct: Vec<CodeUnit> = Vec::new();
for unit in units {
if !distinct
.iter()
.any(|existing| same_visible_symbol(existing, unit))
{
distinct.push(unit.clone());
}
}
distinct
}
pub fn macro_lexical_references_with_visibility<'visibility, 'source: 'visibility, Factory>(
visibility: Factory,
file: &ProjectFile,
root: Node<'_>,
source: &str,
max_references: usize,
cancelled: impl FnMut() -> bool,
) -> MacroLexicalReferences
where
Factory: FnOnce() -> &'visibility VisibilityIndex<'source>,
{
crate::graph::macro_lexical::all_references(
visibility,
file,
root,
source,
max_references,
cancelled,
)
}
impl<'a> VisibilityIndex<'a> {
pub fn data(&self) -> Arc<VisibilityIndexData> {
Arc::clone(&self.data)
}
pub fn from_data(
cpp: &'a dyn CppSource,
token: QueryToken<'a>,
data: Arc<VisibilityIndexData>,
) -> Self {
Self { cpp, token, data }
}
pub fn cpp(&self) -> &'a dyn CppSource {
self.cpp
}
pub fn token(&self) -> QueryToken<'a> {
self.token
}
pub(crate) fn macro_lexical_index(
&self,
file: &ProjectFile,
root: Node<'_>,
source: &str,
) -> Arc<crate::graph::macro_lexical::MacroLexicalIndex> {
let index = bounded_visibility_memo_value(
&self.macro_lexical_indexes,
&self.reads,
file,
MACRO_LEXICAL_INDEX_CACHE_CAP,
|| {
Arc::new(crate::graph::macro_lexical::build_index(
self, file, root, source,
))
},
);
debug_assert_eq!(
index.source_len,
source.len(),
"macro lexical index for {file:?} was built from a different source"
);
index
}
fn callable_metadata(&self, unit: &CodeUnit) -> Arc<Vec<SignatureMetadata>> {
bounded_visibility_memo_value(
&self.callable_metadata,
&self.reads,
unit,
CALLABLE_METADATA_CACHE_CAP,
|| Arc::new(self.cpp.stored_signature_metadata(unit)),
)
}
pub fn callable_linkage(&self, unit: &CodeUnit) -> Option<CallableLinkage> {
crate::identity::cpp_callable_linkage_from_metadata(
self.callable_metadata(unit).iter().cloned(),
)
}
pub fn callable_arity(&self, unit: &CodeUnit) -> Option<CallableArity> {
brokk_bifrost_core::analyzer::model::first_callable_arity(
self.callable_metadata(unit).iter().cloned(),
)
}
pub fn callable_parameter_types(&self, unit: &CodeUnit) -> Option<Vec<String>> {
brokk_bifrost_core::analyzer::model::first_callable_parameter_types(
self.callable_metadata(unit).iter().cloned(),
)
}
pub fn has_unresolved_include_visible_before(
&self,
file: &ProjectFile,
before_byte: usize,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let facts = self.structured_include_facts(file, prepared.as_ref());
has_unresolved_include_visible_before_in_prepared(
file,
prepared.as_ref(),
&self.cpp.include_target_index(),
&facts,
before_byte,
&|guards: Option<&HashSet<PreprocessorGuard>>| {
self.effective_reference_guards(file, guards)
},
)
}
fn structured_include_facts(
&self,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
) -> Arc<[StructuredIncludeFact]> {
visibility_memo_value(
&self.structured_include_fact_cells,
&self.reads,
file.clone(),
|| collect_structured_include_facts(prepared),
)
}
#[cfg(any(test, feature = "test-support"))]
pub fn from_visible_files_for_test(
reads: ReadCapability,
cpp: &'a dyn CppSource,
token: QueryToken<'a>,
visible_by_file: HashMap<ProjectFile, HashSet<CodeUnit>>,
) -> Self {
let visible_source_files_by_root = visible_by_file
.iter()
.map(|(file, visible)| {
(
file.clone(),
visible
.iter()
.map(|unit| unit.source().clone())
.chain(std::iter::once(file.clone()))
.collect(),
)
})
.collect();
let mut global_field_internal_linkage = HashMap::default();
Self {
cpp,
token,
data: Arc::new(VisibilityIndexData {
reads,
visible_by_identifier: build_visible_identifier_index(
&CppGraphSource::from_source(cpp, token),
&visible_by_file,
&visible_source_files_by_root,
&mut global_field_internal_linkage,
),
global_field_internal_linkage,
visible_by_file,
visible_source_files_by_root,
alias_cells: VisibilityMemo::default(),
visible_parser_alias_name_sets: VisibilityMemo::default(),
parser_alias_target_matches: VisibilityMemo::default(),
ordinary_type_import_cells: VisibilityMemo::default(),
project_using_index: ReplayingMemo::new(OnceLock::new()),
namespace_alias_sites: VisibilityMemo::default(),
callable_reference_specs: VisibilityMemo::default(),
structured_include_fact_cells: VisibilityMemo::default(),
include_activation_cells: VisibilityMemo::default(),
compile_proven_guard_cells: VisibilityMemo::default(),
include_path_admission_cells: VisibilityMemo::default(),
conditional_include_projection_cells: VisibilityMemo::default(),
conditional_include_projection_index_build_count: AtomicUsize::new(0),
conditional_include_projection_state_count: AtomicUsize::new(0),
conditional_include_target_state_count: AtomicUsize::new(0),
include_activation_build_count: AtomicUsize::new(0),
using_donor_activation_count: AtomicUsize::new(0),
using_namespace_lookup_count: AtomicUsize::new(0),
using_name_candidate_inspection_count: AtomicUsize::new(0),
callable_reference_spec_build_count: AtomicUsize::new(0),
alias_source_parse_counts: Mutex::new(HashMap::default()),
visible_parser_alias_name_set_build_count: AtomicUsize::new(0),
parser_alias_fallback_calls: AtomicUsize::new(0),
parser_alias_fallback_files: AtomicUsize::new(0),
parser_alias_source_parses: AtomicUsize::new(0),
parser_alias_fallback_elapsed_micros: AtomicUsize::new(0),
field_type_facts: VisibilityMemo::default(),
structured_alias_targets: VisibilityMemo::default(),
callable_comparables: VisibilityMemo::default(),
class_definition_agreements: VisibilityMemo::default(),
callable_metadata: ReplayingMemo::new(Mutex::new(HashMap::default())),
comparable_name_declarations: VisibilityMemo::default(),
indexed_structural_class_scopes: VisibilityMemo::default(),
indexed_enclosing_owner_scopes: VisibilityMemo::default(),
precise_parent_cache: VisibilityMemo::default(),
c_tag_kind_cache: VisibilityMemo::default(),
c_tag_complete_definition_cache: VisibilityMemo::default(),
macro_event_cells: VisibilityMemo::default(),
macro_event_name_sets: VisibilityMemo::default(),
macro_include_protection_cells: IncludeProtectionMemo::default(),
macro_environment_checkpoints: VisibilityMemo::default(),
macro_replacements: VisibilityMemo::default(),
macro_local_binding_templates: VisibilityMemo::default(),
macro_lexical_templates: VisibilityMemo::default(),
macro_lexical_indexes: ReplayingMemo::new(Mutex::new(HashMap::default())),
macro_replacement_bodies: VisibilityMemo::default(),
macro_type_parameters: VisibilityMemo::default(),
callable_parameter_macro_arities: VisibilityMemo::default(),
macro_replacement_parse_count: AtomicUsize::new(0),
macro_event_application_count: AtomicUsize::new(0),
macro_environment_checkpoint_build_count: AtomicUsize::new(0),
macro_environment_copy_count: AtomicUsize::new(0),
macro_environment_request_count: AtomicUsize::new(0),
cpp_template_metadata: HashMap::default(),
cpp_template_families: HashMap::default(),
qualified_candidate_inspections: AtomicUsize::new(0),
target_preserving_type_resolution_count: AtomicUsize::new(0),
visibility_identifier_lookup_count: 0,
visibility_identifier_batch_count: 0,
}),
}
}
fn cpp_source(&self) -> CppGraphSource<'a> {
CppGraphSource::from_source(self.cpp, self.token)
}
pub fn build(
reads: ReadCapability,
cpp: &'a dyn CppSource,
token: QueryToken<'a>,
analyzer: &CppGraphSource<'_>,
roots: &HashSet<ProjectFile>,
) -> Self {
Self::build_with_cancellation(reads, cpp, token, analyzer, roots, None)
}
pub fn build_with_cancellation(
reads: ReadCapability,
cpp: &'a dyn CppSource,
token: QueryToken<'a>,
analyzer: &CppGraphSource<'_>,
roots: &HashSet<ProjectFile>,
cancellation: Option<&CancellationToken>,
) -> Self {
let visibility_started = Instant::now();
let include_targets = cpp.include_target_index();
let includes_started = Instant::now();
let mut include_graph = IncludeGraph::default();
for root in roots {
include_graph.extend_with(root, cancellation, &mut |file| {
cpp.canonical_include_paths(token, file)
.unwrap_or_default()
.into_iter()
.flat_map(|include| {
resolve_include_targets_with_index(file, &include, &include_targets)
})
.collect()
});
}
let include_elapsed = includes_started.elapsed();
let include_file_count = include_graph.files().count();
let visible_source_files_by_root = roots
.iter()
.map(|root| {
(
root.clone(),
include_graph.reachable_files(root, cancellation),
)
})
.collect::<HashMap<_, _>>();
let mut visibility_stats = BoundedVisibilityStats::default();
let mut visible_by_file = build_bounded_visible_declarations(
cpp,
token,
analyzer,
roots,
&visible_source_files_by_root,
cancellation,
&mut visibility_stats,
);
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some() {
eprintln!(
"BIFROST_CPP_VISIBILITY_STATS total_ms={} include_ms={} include_files={} rounds={} root_names={} identifier_lookups={} identifier_batches={} candidate_units={} candidate_sources={} declaration_reads={} declaration_units={} selected_units={} dependency_source_names={} dependency_names={} lookup_ms={} declaration_ms={} dependency_source_ms={}",
visibility_started.elapsed().as_millis(),
include_elapsed.as_millis(),
include_file_count,
visibility_stats.rounds,
visibility_stats.root_names,
visibility_stats.identifier_lookups,
visibility_stats.identifier_batches,
visibility_stats.candidate_units,
visibility_stats.candidate_sources,
visibility_stats.declaration_reads,
visibility_stats.declaration_units,
visibility_stats.selected_units,
visibility_stats.dependency_source_names,
visibility_stats.dependency_names,
visibility_stats.lookup_elapsed.as_millis(),
visibility_stats.declaration_elapsed.as_millis(),
visibility_stats.dependency_source_elapsed.as_millis(),
);
}
let report_stats = std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some();
let finalize_started = Instant::now();
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS status=started roots={} visible_units={}",
visible_by_file.len(),
visible_by_file.values().map(HashSet::len).sum::<usize>(),
);
}
let owner_started = Instant::now();
if report_stats {
eprintln!("BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=owners status=started");
}
let owner_stats = extend_with_out_of_line_owner_bindings(cpp, &mut visible_by_file);
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=owners status=completed unseen_owners={} definition_lookups={} admitted={} elapsed_ms={}",
owner_stats.unseen_owners,
owner_stats.definition_lookups,
owner_stats.admitted,
owner_started.elapsed().as_millis(),
);
}
let mut global_field_internal_linkage = HashMap::default();
let identifier_started = Instant::now();
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=identifier_index status=started"
);
}
let visible_by_identifier = build_visible_identifier_index(
analyzer,
&visible_by_file,
&visible_source_files_by_root,
&mut global_field_internal_linkage,
);
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=identifier_index status=completed roots={} names={} candidates={} elapsed_ms={}",
visible_by_identifier.len(),
visible_by_identifier
.values()
.map(HashMap::len)
.sum::<usize>(),
visible_by_identifier
.values()
.flat_map(HashMap::values)
.map(Vec::len)
.sum::<usize>(),
identifier_started.elapsed().as_millis(),
);
}
let mut cpp_template_metadata = HashMap::default();
let metadata_started = Instant::now();
let mut template_classes = 0usize;
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=template_metadata status=started"
);
}
for unit in visible_by_file
.values()
.flatten()
.filter(|unit| unit.is_class())
{
template_classes += 1;
if cpp_template_metadata.contains_key(unit) {
continue;
}
if let Some(metadata) = cpp.template_metadata(unit) {
cpp_template_metadata.insert(unit.clone(), metadata);
}
}
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=template_metadata status=completed classes={} metadata={} elapsed_ms={}",
template_classes,
cpp_template_metadata.len(),
metadata_started.elapsed().as_millis(),
);
}
let families_started = Instant::now();
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=template_families status=started"
);
}
let mut cpp_template_families: HashMap<String, Vec<CodeUnit>> = HashMap::default();
for (unit, metadata) in &cpp_template_metadata {
cpp_template_families
.entry(metadata.primary_fq_name.clone())
.or_default()
.push(unit.clone());
}
for family in cpp_template_families.values_mut() {
sort_lookup_units(family);
}
if report_stats {
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS phase=template_families status=completed families={} members={} elapsed_ms={}",
cpp_template_families.len(),
cpp_template_families.values().map(Vec::len).sum::<usize>(),
families_started.elapsed().as_millis(),
);
eprintln!(
"BIFROST_CPP_VISIBILITY_FINALIZE_STATS status=completed roots={} visible_units={} elapsed_ms={} total_ms={}",
visible_by_file.len(),
visible_by_file.values().map(HashSet::len).sum::<usize>(),
finalize_started.elapsed().as_millis(),
visibility_started.elapsed().as_millis(),
);
}
Self {
cpp,
token,
data: Arc::new(VisibilityIndexData {
reads,
visible_by_file,
visible_by_identifier,
global_field_internal_linkage,
visible_source_files_by_root,
alias_cells: VisibilityMemo::default(),
visible_parser_alias_name_sets: VisibilityMemo::default(),
parser_alias_target_matches: VisibilityMemo::default(),
ordinary_type_import_cells: VisibilityMemo::default(),
project_using_index: ReplayingMemo::new(OnceLock::new()),
namespace_alias_sites: VisibilityMemo::default(),
callable_reference_specs: VisibilityMemo::default(),
structured_include_fact_cells: VisibilityMemo::default(),
include_activation_cells: VisibilityMemo::default(),
compile_proven_guard_cells: VisibilityMemo::default(),
include_path_admission_cells: VisibilityMemo::default(),
conditional_include_projection_cells: VisibilityMemo::default(),
#[cfg(any(test, feature = "test-support"))]
conditional_include_projection_index_build_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
conditional_include_projection_state_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
conditional_include_target_state_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
include_activation_build_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
using_donor_activation_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
using_namespace_lookup_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
using_name_candidate_inspection_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
callable_reference_spec_build_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
alias_source_parse_counts: Mutex::new(HashMap::default()),
#[cfg(any(test, feature = "test-support"))]
visible_parser_alias_name_set_build_count: AtomicUsize::new(0),
parser_alias_fallback_calls: AtomicUsize::new(0),
parser_alias_fallback_files: AtomicUsize::new(0),
parser_alias_source_parses: AtomicUsize::new(0),
parser_alias_fallback_elapsed_micros: AtomicUsize::new(0),
field_type_facts: VisibilityMemo::default(),
structured_alias_targets: VisibilityMemo::default(),
callable_comparables: VisibilityMemo::default(),
class_definition_agreements: VisibilityMemo::default(),
callable_metadata: ReplayingMemo::new(Mutex::new(HashMap::default())),
comparable_name_declarations: VisibilityMemo::default(),
indexed_structural_class_scopes: VisibilityMemo::default(),
indexed_enclosing_owner_scopes: VisibilityMemo::default(),
precise_parent_cache: VisibilityMemo::default(),
c_tag_kind_cache: VisibilityMemo::default(),
c_tag_complete_definition_cache: VisibilityMemo::default(),
macro_event_cells: VisibilityMemo::default(),
macro_event_name_sets: VisibilityMemo::default(),
macro_include_protection_cells: IncludeProtectionMemo::default(),
macro_environment_checkpoints: VisibilityMemo::default(),
macro_replacements: VisibilityMemo::default(),
macro_local_binding_templates: VisibilityMemo::default(),
macro_lexical_templates: VisibilityMemo::default(),
macro_lexical_indexes: ReplayingMemo::new(Mutex::new(HashMap::default())),
macro_replacement_bodies: VisibilityMemo::default(),
macro_type_parameters: VisibilityMemo::default(),
callable_parameter_macro_arities: VisibilityMemo::default(),
#[cfg(any(test, feature = "test-support"))]
macro_replacement_parse_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
macro_event_application_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
macro_environment_checkpoint_build_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
macro_environment_copy_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
macro_environment_request_count: AtomicUsize::new(0),
cpp_template_metadata,
cpp_template_families,
#[cfg(any(test, feature = "test-support"))]
qualified_candidate_inspections: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
target_preserving_type_resolution_count: AtomicUsize::new(0),
#[cfg(any(test, feature = "test-support"))]
visibility_identifier_lookup_count: visibility_stats.identifier_lookups,
#[cfg(any(test, feature = "test-support"))]
visibility_identifier_batch_count: visibility_stats.identifier_batches,
}),
}
}
pub fn is_visible(&self, file: &ProjectFile, target: &CodeUnit) -> bool {
if file == target.source() {
return true;
}
if self.global_field_has_internal_linkage(target) {
return self
.visible_source_files_by_root
.get(file)
.is_some_and(|sources| sources.contains(target.source()));
}
self.visible_by_file
.get(file)
.is_some_and(|visible| visible.iter().any(|unit| same_visible_symbol(unit, target)))
}
fn global_field_has_internal_linkage(&self, unit: &CodeUnit) -> bool {
self.global_field_internal_linkage
.get(unit)
.copied()
.unwrap_or_else(|| cpp_global_field_has_internal_linkage(&self.cpp_source(), unit))
}
pub fn call_arity_evidence(
&self,
file: &ProjectFile,
call: Node<'_>,
source: &str,
) -> CallArityEvidence {
self.call_arity_evidence_at(file, call, source, call.start_byte())
}
pub fn call_arity_evidence_at(
&self,
file: &ProjectFile,
call: Node<'_>,
source: &str,
environment_byte: usize,
) -> CallArityEvidence {
let Some(arguments) = call
.child_by_field_name("arguments")
.or_else(|| call.child_by_field_name("parameters"))
.or_else(|| call.child_by_field_name("value"))
.or_else(|| first_named_child_of_kind(call, "argument_list"))
.or_else(|| first_named_child_of_kind(call, "initializer_list"))
else {
return CallArityEvidence::Exact(0);
};
let recovered_c_keyword_arguments =
recovered_c_keyword_argument_count(file, call, arguments, source);
let c_semantics = reference_uses_c_semantics(self.cpp, file);
let arguments = argument_children(arguments)
.flat_map(|argument| {
recovered_c_new_expression_arguments(argument, c_semantics)
.map(Vec::from)
.unwrap_or_else(|| vec![argument])
})
.collect::<Vec<_>>();
if arguments
.iter()
.all(|argument| !argument_shape_may_change_arity(*argument))
{
return CallArityEvidence::Exact(arguments.len() + recovered_c_keyword_arguments);
}
let environment = self.macro_environment(file, environment_byte);
let mut stack = Vec::new();
let mut total = recovered_c_keyword_arguments;
for argument in arguments {
if !macro_expansion_shape_is_safe(argument, source, &[], &environment) {
return CallArityEvidence::Unknown;
}
let CallArityEvidence::Exact(spread) =
self.argument_arity_evidence(argument, source, &environment, &mut stack)
else {
return CallArityEvidence::Unknown;
};
total += spread;
}
CallArityEvidence::Exact(total)
}
fn argument_arity_evidence(
&self,
argument: Node<'_>,
source: &str,
environment: &MacroEnvironment,
stack: &mut Vec<(ProjectFile, usize)>,
) -> CallArityEvidence {
let (name, invocation_arguments, function_like) = match argument.kind() {
"identifier" => (node_text(argument, source), None, false),
"call_expression" => {
let Some(function) = argument.child_by_field_name("function") else {
return CallArityEvidence::Exact(1);
};
if function.kind() != "identifier" {
return CallArityEvidence::Exact(1);
}
let Some(arguments) = argument.child_by_field_name("arguments") else {
return CallArityEvidence::Exact(1);
};
(node_text(function, source), Some(arguments), true)
}
_ => return CallArityEvidence::Exact(1),
};
let Some(binding) = environment.binding(name) else {
return if environment.unknown_names {
CallArityEvidence::Unknown
} else {
CallArityEvidence::Exact(1)
};
};
if !binding.is_exact() {
return CallArityEvidence::Unknown;
}
match (&binding.definition, invocation_arguments, function_like) {
(MacroDefinition::Object { replacement }, None, false) => self
.replacement_arity_evidence(
replacement,
&[],
&[],
source,
environment,
stack,
binding,
),
(
MacroDefinition::Function {
parameters,
replacement,
},
Some(arguments),
true,
) => {
let actuals = argument_children(arguments).collect::<Vec<_>>();
if actuals.len() != parameters.len() {
CallArityEvidence::Unknown
} else {
self.replacement_arity_evidence(
replacement,
parameters,
&actuals,
source,
environment,
stack,
binding,
)
}
}
(MacroDefinition::Function { .. }, None, false) => CallArityEvidence::Exact(1),
_ => CallArityEvidence::Unknown,
}
}
#[allow(clippy::too_many_arguments)]
fn replacement_arity_evidence(
&self,
replacement: &str,
parameters: &[String],
actuals: &[Node<'_>],
actual_source: &str,
environment: &MacroEnvironment,
stack: &mut Vec<(ProjectFile, usize)>,
binding: &MacroBinding,
) -> CallArityEvidence {
let identity = (binding.source.clone(), binding.declaration_byte);
if stack.contains(&identity) || replacement.trim().is_empty() {
return CallArityEvidence::Unknown;
}
stack.push(identity);
let parsed = self.parsed_macro_replacement(binding, replacement);
let evidence = (|| {
let ParsedMacroReplacement::Parsed {
source: sentinel,
tree,
} = parsed.as_ref()
else {
return None;
};
let call = first_descendant_of_kind(tree.root_node(), "call_expression")?;
let arguments = call.child_by_field_name("arguments")?;
let mut total = 0usize;
for argument in argument_children(arguments) {
if !macro_expansion_shape_is_safe(argument, sentinel, parameters, environment) {
return None;
}
if argument.kind() == "identifier"
&& let Some(parameter_index) = parameters
.iter()
.position(|parameter| parameter == node_text(argument, sentinel))
{
if !macro_expansion_shape_is_safe(
actuals[parameter_index],
actual_source,
&[],
environment,
) {
return None;
}
let CallArityEvidence::Exact(spread) = self.argument_arity_evidence(
actuals[parameter_index],
actual_source,
environment,
stack,
) else {
return None;
};
total += spread;
continue;
}
let CallArityEvidence::Exact(spread) =
self.argument_arity_evidence(argument, sentinel, environment, stack)
else {
return None;
};
total += spread;
}
Some(CallArityEvidence::Exact(total))
})()
.unwrap_or(CallArityEvidence::Unknown);
stack.pop();
evidence
}
fn parsed_macro_replacement(
&self,
binding: &MacroBinding,
replacement: &str,
) -> Arc<ParsedMacroReplacement> {
visibility_memo_value(
&self.macro_replacements,
&self.reads,
(binding.source.clone(), binding.declaration_byte),
|| {
#[cfg(any(test, feature = "test-support"))]
self.macro_replacement_parse_count
.fetch_add(1, Ordering::Relaxed);
let parsed = parse_sentinel(
"void __bifrost_macro_arity() { __bifrost_macro_call(",
SentinelInput::replacement(replacement),
"); }",
);
let parsed = if parsed.tree.root_node().has_error() {
ParsedMacroReplacement::Unsupported
} else {
ParsedMacroReplacement::Parsed {
source: parsed.source,
tree: parsed.tree,
}
};
Arc::new(parsed)
},
)
}
pub fn function_macro_local_binding<'tree>(
&self,
file: &ProjectFile,
statement: Node<'tree>,
source: &str,
) -> Option<MacroLocalBinding<'tree>> {
if !is_c_source_file(file) {
return None;
}
if let Some(binding) = recognized_c_macro_declarator_binding(statement, source) {
return Some(binding);
}
let call = match statement.kind() {
"expression_statement" => {
single_named_child_matching(statement, |child| !child.is_extra())?
}
_ => statement,
};
let (function, actuals) = function_macro_invocation_parts(call)?;
let environment = self.macro_environment(file, call.start_byte());
let function_name = node_text(function, source);
let binding = environment.binding(function_name)?;
let MacroDefinition::Function {
parameters,
replacement,
} = &binding.definition
else {
return None;
};
if actuals.len() != parameters.len() {
return None;
}
let template = self.macro_local_binding_template(binding, parameters, replacement)?;
let (type_name, type_node) = match &template.declared_type {
MacroLocalBindingTypeTemplate::Parameter(index) => {
let actual = *actuals.get(*index)?;
if !macro_expansion_shape_is_safe(actual, source, &[], &environment) {
return None;
}
let type_node = macro_type_argument_node(actual, source).unwrap_or(actual);
(
node_text(actual, source).trim().to_string(),
Some(type_node),
)
}
MacroLocalBindingTypeTemplate::Fixed(type_name) => (type_name.clone(), None),
};
if type_name.is_empty() {
return None;
}
Some(MacroLocalBinding {
name: template.name.clone(),
type_name,
type_node,
pointer_depth: template.pointer_depth,
proven_unit: None,
})
}
pub fn function_macro_container_binding<'tree>(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
assignment: Node<'tree>,
source: &str,
) -> Option<MacroLocalBinding<'tree>> {
if !is_c_source_file(file) || assignment.kind() != "assignment_expression" {
return None;
}
let name_node = assignment.child_by_field_name("left")?;
if name_node.kind() != "identifier" {
return None;
}
let call = assignment.child_by_field_name("right")?;
if call.kind() != "call_expression" {
return None;
}
let function = call.child_by_field_name("function")?;
if function.kind() != "identifier" {
return None;
}
let arguments = call.child_by_field_name("arguments")?;
let actuals = argument_children(arguments).collect::<Vec<_>>();
let function_name = node_text(function, source);
let environment = self.macro_environment(file, call.start_byte());
let binding = environment.binding(function_name)?;
if !binding.is_exact() {
return None;
}
let MacroDefinition::Function {
parameters,
replacement,
} = &binding.definition
else {
return None;
};
if actuals.len() != parameters.len() {
return None;
}
let body = self.parsed_macro_replacement_body(
&(binding.source.clone(), binding.declaration_byte),
parameters,
replacement,
)?;
let type_parameter = macro_replacement_type_parameter(&body, parameters)?;
let type_argument = *actuals.get(type_parameter)?;
let type_node = macro_type_argument_node(type_argument, source)?;
let type_name = node_text(type_node, source).trim().to_string();
if type_name.is_empty() {
return None;
}
let explicit_tag = match node_text(type_argument, source) {
"struct" => Some(CppCTagKind::Struct),
"union" => Some(CppCTagKind::Union),
_ => None,
};
let resolved_type = if let Some(tag) = explicit_tag {
let candidates = self
.visible_identifier_candidates(file, &type_name)
.filter(|candidate| self.cached_c_tag_kind(analyzer, candidate) == Some(tag))
.collect::<Vec<_>>();
self.resolve_type_candidates(
analyzer,
file,
&candidates,
TypeCandidateResolution::Canonical,
)
.ok()
} else {
self.resolve_type_node_result(file, type_node, source)
.ok()
.flatten()
};
let proven_unit = resolved_type?;
Some(MacroLocalBinding {
name: node_text(name_node, source).to_string(),
type_name,
type_node: Some(type_node),
pointer_depth: 1,
proven_unit: Some(proven_unit),
})
}
pub fn macro_local_binding_at<'tree>(
&self,
file: &ProjectFile,
root: Node<'tree>,
source: &str,
start_byte: usize,
end_byte: usize,
) -> Option<MacroLocalBinding<'tree>> {
crate::graph::macro_lexical::typed_binding(self, file, root, source, start_byte, end_byte)
}
pub fn macro_lexical_binding(
&self,
file: &ProjectFile,
root: Node<'_>,
source: &str,
start_byte: usize,
end_byte: usize,
) -> Option<MacroLexicalBinding> {
crate::graph::macro_lexical::binding(self, file, root, source, start_byte, end_byte)
}
pub fn macro_lexical_references(
&self,
file: &ProjectFile,
root: Node<'_>,
source: &str,
max_references: usize,
cancelled: impl FnMut() -> bool,
) -> MacroLexicalReferences {
crate::graph::macro_lexical::all_references(
|| self,
file,
root,
source,
max_references,
cancelled,
)
}
pub(crate) fn function_macro_binding_at(
&self,
file: &ProjectFile,
name: &str,
before_byte: usize,
) -> Option<(ProjectFile, usize)> {
let environment = self.macro_environment(file, before_byte);
let binding = environment.binding(name)?;
if binding.is_exact()
&& matches!(
binding.definition,
MacroDefinition::Function { .. } | MacroDefinition::VariadicFunction { .. }
)
{
return Some((binding.source.clone(), binding.declaration_byte));
}
if binding.source != *file {
return None;
}
let prepared = self.cpp.prepared_syntax(self.token, file)?;
let root = prepared.tree().root_node();
let source = prepared.source();
let reference = root.descendant_for_byte_range(
before_byte,
before_byte.saturating_add(1).min(source.len()),
)?;
let reference_conditions = owning_preprocessor_conditionals(root, reference, source);
let cell = self.macro_event_cell(file);
let events = cell.get_or_init(|| Arc::from(self.collect_macro_events(file)));
let event = events
.iter()
.find(|event| event.byte() == binding.declaration_byte)?;
let MacroEvent::Define {
name: defined_name,
binding: definition,
conditionals,
..
} = event
else {
return None;
};
if defined_name != name
|| conditionals.is_empty()
|| !conditionals
.iter()
.all(|condition| reference_conditions.contains(condition))
|| !matches!(
definition.definition,
MacroDefinition::Function { .. } | MacroDefinition::VariadicFunction { .. }
)
{
return None;
}
Some((definition.source.clone(), definition.declaration_byte))
}
pub fn function_macro_type_argument<'tree>(
&self,
file: &ProjectFile,
node: Node<'tree>,
source: &str,
) -> Option<Node<'tree>> {
if !is_c_source_file(file) {
return None;
}
let (call, argument_index, argument) = c_function_macro_argument(node)?;
let function = call.child_by_field_name("function")?;
let function_name = node_text(function, source);
if function_name.is_empty() {
return None;
}
if !self.file_defines_macro_name(file, function_name)
&& !self
.visible_identifier_candidates(file, function_name)
.any(|candidate| candidate.is_macro())
{
return None;
}
let environment = self.macro_environment(file, call.start_byte());
let binding = environment.binding(function_name)?;
if !binding.is_exact() {
return None;
}
let (parameters, replacement, variadic) = match &binding.definition {
MacroDefinition::Function {
parameters,
replacement,
} => (parameters, replacement, false),
MacroDefinition::VariadicFunction {
parameters,
replacement,
} => (parameters, replacement, true),
MacroDefinition::Object { .. } | MacroDefinition::Unsupported => return None,
};
let actuals = call
.child_by_field_name("arguments")
.map(argument_children)
.into_iter()
.flatten()
.collect::<Vec<_>>();
let arity_matches = if variadic {
actuals.len() >= parameters.len()
} else {
actuals.len() == parameters.len()
};
let type_parameters = self.macro_type_parameter_indices(
&(binding.source.clone(), binding.declaration_byte),
parameters,
replacement,
)?;
if !arity_matches || !type_parameters.contains(&argument_index) {
return None;
}
let type_node = macro_type_argument_node(argument, source)?;
if self.names_a_macro_at(file, node_text(type_node, source), type_node.start_byte()) {
return None;
}
Some(type_node)
}
fn macro_local_binding_template(
&self,
binding: &MacroBinding,
parameters: &[String],
replacement: &str,
) -> Option<Arc<MacroLocalBindingTemplate>> {
visibility_memo_value(
&self.macro_local_binding_templates,
&self.reads,
(binding.source.clone(), binding.declaration_byte),
|| {
let key = (binding.source.clone(), binding.declaration_byte);
let body = self.parsed_macro_replacement_body(&key, parameters, replacement)?;
let sentinel = body.source.as_str();
let statements = body.statements()?;
let declaration =
single_named_child_matching(statements, |child| !child.is_extra())?;
if declaration.kind() != "declaration" {
return None;
}
let type_node = declaration
.child_by_field_name("type")
.or_else(|| first_type_child(declaration))?;
let declarator = declaration.child_by_field_name("declarator").or_else(|| {
let mut cursor = declaration.walk();
declaration.named_children(&mut cursor).find_map(|child| {
if child.kind() == "init_declarator" {
child.child_by_field_name("declarator")
} else {
is_declarator_node(child).then_some(child)
}
})
})?;
let name = extract_variable_name(declarator, sentinel)?;
let pointer_depth =
declared_name_binding(declaration, type_node, &name, sentinel)?.pointer_depth;
let type_text = node_text(type_node, sentinel).trim();
let declared_type = parameters
.iter()
.position(|parameter| parameter == type_text)
.map(MacroLocalBindingTypeTemplate::Parameter)
.unwrap_or_else(|| MacroLocalBindingTypeTemplate::Fixed(type_text.to_string()));
Some(Arc::new(MacroLocalBindingTemplate {
name,
declared_type,
pointer_depth,
}))
},
)
}
pub fn function_macro_replacement_body(
&self,
file: &ProjectFile,
definition: Node<'_>,
source: &str,
) -> Option<Arc<ParsedReplacementBody>> {
debug_assert_eq!(definition.kind(), "preproc_function_def");
let (parameters, replacement) = match Self::decode_macro_definition(definition, source) {
MacroDefinition::Function {
parameters,
replacement,
}
| MacroDefinition::VariadicFunction {
parameters,
replacement,
} => (parameters, replacement),
MacroDefinition::Object { .. } | MacroDefinition::Unsupported => return None,
};
self.parsed_macro_replacement_body(
&(file.clone(), definition.start_byte()),
¶meters,
&replacement,
)
}
fn parsed_macro_replacement_body(
&self,
key: &(ProjectFile, usize),
parameters: &[String],
replacement: &str,
) -> Option<Arc<ParsedReplacementBody>> {
visibility_memo_value(
&self.macro_replacement_bodies,
&self.reads,
key.clone(),
|| {
if replacement.trim().is_empty() {
return None;
}
let (source, tree, original_offsets) =
Self::parse_macro_replacement_body(replacement, parameters)?;
let body = ParsedReplacementBody {
source,
tree,
body_offset: MACRO_BODY_SENTINEL_PREFIX.len(),
parameters: parameters.to_vec(),
original_offsets,
};
body.statements()?;
if body.expands_variadic_arguments() {
return None;
}
Some(Arc::new(body))
},
)
}
fn parse_macro_replacement_body(
replacement: &str,
parameters: &[String],
) -> Option<(String, Tree, MacroOriginMap)> {
let parsed = parse_sentinel(
MACRO_BODY_SENTINEL_PREFIX,
SentinelInput::replacement(replacement),
MACRO_BODY_SENTINEL_SUFFIX,
);
let normalized_end = parsed.source.len() - MACRO_BODY_SENTINEL_SUFFIX.len() - 1;
let normalized = &parsed.source[MACRO_BODY_SENTINEL_PREFIX.len()..normalized_end];
let normalized_offsets = &parsed.original_offsets;
let source = parsed.source.as_str();
let tree = &parsed.tree;
if !tree.root_node().has_error() {
return Some((
parsed.source,
parsed.tree,
parsed.original_offsets.into_boxed_slice(),
));
}
let body_offset = MACRO_BODY_SENTINEL_PREFIX.len();
let mut insertion_points = Vec::new();
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
if matches!(
node.kind(),
"identifier" | "type_identifier" | "field_identifier" | "namespace_identifier"
) && parameters
.iter()
.any(|parameter| parameter.as_str() == node_text(node, source))
&& Self::macro_formal_needs_statement_separator(node)
{
let point = node.end_byte().saturating_sub(body_offset);
if point <= normalized.len() && !insertion_points.contains(&point) {
insertion_points.push(point);
}
}
push_named_children_reversed(node, &mut stack);
}
if insertion_points.is_empty() {
return None;
}
insertion_points.sort_unstable();
let mut recovered = Vec::with_capacity(normalized.len() + insertion_points.len());
let mut original_offsets =
Vec::with_capacity(normalized.len() + insertion_points.len() + 1);
let mut next_insertion = 0;
for (index, byte) in normalized.bytes().enumerate() {
recovered.push(byte);
original_offsets.push(normalized_offsets[index].clone());
while insertion_points.get(next_insertion).copied() == Some(index + 1) {
recovered.push(b';');
let boundary = normalized_offsets[index].end;
original_offsets.push(boundary..boundary);
next_insertion += 1;
}
}
original_offsets.push(replacement.len()..replacement.len());
let recovered = String::from_utf8(recovered).expect("source text remains UTF-8");
let parsed = parse_sentinel(
MACRO_BODY_SENTINEL_PREFIX,
SentinelInput {
source: recovered,
original_offsets,
},
MACRO_BODY_SENTINEL_SUFFIX,
);
let (source, tree, original_offsets) =
(parsed.source, parsed.tree, parsed.original_offsets);
if tree.root_node().has_error() {
return None;
}
Some((source, tree, original_offsets.into_boxed_slice()))
}
fn macro_formal_needs_statement_separator(node: Node<'_>) -> bool {
let mut current = node;
let mut crossed_recovery = false;
while let Some(parent) = current.parent() {
if parent.is_error() {
crossed_recovery = true;
current = parent;
continue;
}
if matches!(
parent.kind(),
"call_expression"
| "argument_list"
| "field_expression"
| "binary_expression"
| "unary_expression"
| "assignment_expression"
| "conditional_expression"
| "parenthesized_expression"
| "subscript_expression"
) {
return false;
}
if parent.kind() == "expression_statement" {
return single_named_child_matching(parent, |child| !child.is_extra()).is_some();
}
if parent.kind() == "compound_statement" && current == node {
return true;
}
if matches!(
parent.kind(),
"compound_statement" | "if_statement" | "while_statement" | "do_statement"
) {
return crossed_recovery
|| parent.child_by_field_name("consequence") == Some(current);
}
if matches!(parent.kind(), "declaration" | "init_declarator") {
return false;
}
current = parent;
}
false
}
fn macro_type_parameter_indices(
&self,
key: &(ProjectFile, usize),
parameters: &[String],
replacement: &str,
) -> Option<Arc<[usize]>> {
visibility_memo_value(
&self.macro_type_parameters,
&self.reads,
key.clone(),
|| {
#[cfg(any(test, feature = "test-support"))]
self.macro_replacement_parse_count
.fetch_add(1, Ordering::Relaxed);
if replacement.trim().is_empty() {
return None;
}
let parsed = parse_sentinel(
MACRO_BODY_SENTINEL_PREFIX,
SentinelInput::replacement(replacement),
MACRO_BODY_SENTINEL_SUFFIX,
);
let (source, tree, original_offsets) =
(parsed.source, parsed.tree, parsed.original_offsets);
let body = ParsedReplacementBody {
source,
tree,
body_offset: MACRO_BODY_SENTINEL_PREFIX.len(),
parameters: parameters.to_vec(),
original_offsets: original_offsets.into_boxed_slice(),
};
macro_replacement_type_parameters(&body, parameters).map(Arc::from)
},
)
}
fn decode_macro_definition(node: Node<'_>, source: &str) -> MacroDefinition {
let replacement = if node.kind() == "preproc_function_def" {
function_macro_replacement_span(node, source)
.and_then(|span| source.get(span))
.map(str::to_owned)
.or_else(|| {
node.child_by_field_name("value")
.map(|value| node_text(value, source).to_string())
})
.unwrap_or_default()
} else {
node.child_by_field_name("value")
.map(|value| node_text(value, source).to_string())
.unwrap_or_default()
};
if node.kind() == "preproc_def" {
return MacroDefinition::Object { replacement };
}
let Some(parameters) = node.child_by_field_name("parameters") else {
return MacroDefinition::Unsupported;
};
let variadic = (0..parameters.child_count()).any(|index| {
parameters
.child(index)
.is_some_and(|child| child.kind() == "...")
});
let parameters = (0..parameters.named_child_count())
.filter_map(|index| parameters.named_child(index))
.map(|parameter| node_text(parameter, source).to_string())
.collect::<Vec<_>>();
if variadic {
MacroDefinition::VariadicFunction {
parameters,
replacement,
}
} else {
MacroDefinition::Function {
parameters,
replacement,
}
}
}
pub fn macro_event_cell(&self, file: &ProjectFile) -> MacroEventCell {
visibility_memo_value(&self.macro_event_cells, &self.reads, file.clone(), || {
Arc::new(VisibilityCell::new(self.reads.clone()))
})
}
fn file_defines_macro_name(&self, file: &ProjectFile, name: &str) -> bool {
let names = visibility_memo_value(
&self.macro_event_name_sets,
&self.reads,
file.clone(),
|| {
let cell = self.macro_event_cell(file);
let events = cell.get_or_init(|| Arc::from(self.collect_macro_events(file)));
Arc::new(
events
.iter()
.filter_map(|event| match event {
MacroEvent::Define { name, .. } => Some(name.clone()),
MacroEvent::Undef { .. }
| MacroEvent::Include { .. }
| MacroEvent::Invalidate { .. } => None,
})
.collect(),
)
},
);
names.contains(name)
}
fn macro_environment_checkpoint_cell(
&self,
file: &ProjectFile,
) -> MacroEnvironmentCheckpointCell {
visibility_memo_value(
&self.macro_environment_checkpoints,
&self.reads,
file.clone(),
|| Arc::new(VisibilityCell::new(self.reads.clone())),
)
}
pub fn macro_environment(
&self,
file: &ProjectFile,
before_byte: usize,
) -> Arc<MacroEnvironment> {
#[cfg(any(test, feature = "test-support"))]
self.macro_environment_request_count
.fetch_add(1, Ordering::Relaxed);
let cell = self.macro_event_cell(file);
let events = cell.get_or_init(|| Arc::from(self.collect_macro_events(file)));
let frontier = events.partition_point(|event| event.byte() < before_byte);
let checkpoint_cell = self.macro_environment_checkpoint_cell(file);
let checkpoints = checkpoint_cell
.get_or_init(|| Arc::new(self.build_macro_environment_checkpoints(file, &events)));
let checkpoint = checkpoints.at_or_before(frontier);
if checkpoint.frontier == frontier {
return Arc::clone(&checkpoint.environment);
}
#[cfg(any(test, feature = "test-support"))]
self.macro_environment_copy_count
.fetch_add(1, Ordering::Relaxed);
let mut environment = checkpoint.environment.as_ref().clone();
let mut include_stack = HashSet::from_iter([file.clone()]);
for event in &events[checkpoint.frontier..frontier] {
self.apply_macro_event(file, event, &mut environment, &mut include_stack);
}
Arc::new(environment)
}
fn build_macro_environment_checkpoints(
&self,
file: &ProjectFile,
events: &[MacroEvent],
) -> MacroEnvironmentCheckpoints {
#[cfg(any(test, feature = "test-support"))]
self.macro_environment_checkpoint_build_count
.fetch_add(1, Ordering::Relaxed);
let mut environment = MacroEnvironment {
build_proven_defines: self
.compile_proven_guards(file)
.iter()
.filter_map(|guard| match guard {
PreprocessorGuard::Defined(name) => Some(name.clone()),
_ => None,
})
.collect(),
..MacroEnvironment::default()
};
let mut checkpoints = vec![MacroEnvironmentCheckpoint {
frontier: 0,
environment: Arc::new(environment.clone()),
}];
let checkpoint_stride = events
.len()
.div_ceil(MACRO_ENVIRONMENT_CHECKPOINT_STRIDE)
.clamp(1, MACRO_ENVIRONMENT_CHECKPOINT_STRIDE);
let mut include_stack = HashSet::from_iter([file.clone()]);
for (index, event) in events.iter().enumerate() {
self.apply_macro_event(file, event, &mut environment, &mut include_stack);
let frontier = index + 1;
if frontier % checkpoint_stride == 0 || matches!(event, MacroEvent::Include { .. }) {
checkpoints.push(MacroEnvironmentCheckpoint {
frontier,
environment: Arc::new(environment.clone()),
});
}
}
MacroEnvironmentCheckpoints { checkpoints }
}
pub fn names_a_macro_at(&self, file: &ProjectFile, name: &str, before_byte: usize) -> bool {
self.macro_environment(file, before_byte)
.binding(name)
.is_some()
}
pub fn macro_name_may_be_bound_at(
&self,
file: &ProjectFile,
name: &str,
before_byte: usize,
) -> bool {
self.macro_environment(file, before_byte).may_bind(name)
}
pub fn macro_binding_matches_target_at(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
name: &str,
before_byte: usize,
target: &CodeUnit,
) -> bool {
let ranges = analyzer.ranges(target);
let declaration_bytes = self.macro_declaration_bytes(target, &ranges);
self.macro_binding_matches_target_declaration_at(
file,
name,
before_byte,
target.source(),
&declaration_bytes,
)
}
pub(crate) fn macro_declaration_bytes(
&self,
target: &CodeUnit,
ranges: &[Range],
) -> Vec<usize> {
let Some(prepared) = self.cpp.prepared_syntax(self.token, target.source()) else {
return Vec::new();
};
ranges
.iter()
.filter_map(|range| {
let mut node = node_for_exact_range(prepared.tree().root_node(), range)?;
while !matches!(node.kind(), "preproc_def" | "preproc_function_def") {
node = node.parent()?;
}
Some(node.start_byte())
})
.collect()
}
pub(crate) fn macro_binding_matches_target_declaration_at(
&self,
file: &ProjectFile,
name: &str,
before_byte: usize,
target_source: &ProjectFile,
target_declaration_bytes: &[usize],
) -> bool {
let environment = self.macro_environment(file, before_byte);
let Some(binding) = environment.binding(name) else {
return false;
};
if binding.definition == MacroDefinition::Unsupported {
return false;
}
if binding.source != *target_source {
return false;
}
target_declaration_bytes.contains(&binding.declaration_byte)
}
pub fn resolve_ordinary_macro_reference(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
node: Node<'_>,
source: &str,
) -> OrdinaryMacroReferenceResolution {
if !is_ordinary_macro_reference_node(node) {
return OrdinaryMacroReferenceResolution::Missing;
}
let name = node_text(node, source);
if name.is_empty() {
return OrdinaryMacroReferenceResolution::Missing;
}
let visible = self
.visible_identifier_candidates(file, name)
.filter(|candidate| candidate.is_macro())
.cloned()
.collect::<Vec<_>>();
let mut exact = Vec::new();
for candidate in &visible {
if self.macro_binding_matches_target_at(
analyzer,
file,
name,
node.start_byte(),
candidate,
) && !exact
.iter()
.any(|existing| same_visible_symbol(existing, candidate))
{
exact.push(candidate.clone());
}
}
match exact.len() {
1 => OrdinaryMacroReferenceResolution::Resolved(exact.pop().unwrap()),
2.. => OrdinaryMacroReferenceResolution::Ambiguous,
0 if !visible.is_empty()
&& self.macro_name_may_be_bound_at(file, name, node.start_byte()) =>
{
OrdinaryMacroReferenceResolution::Ambiguous
}
0 => OrdinaryMacroReferenceResolution::Missing,
}
}
pub fn recovered_c_reference_ranges(
&self,
file: &ProjectFile,
root: Node<'_>,
source: &str,
limit: usize,
) -> RecoveredCReferenceRanges {
if !is_c_source_file(file) {
return RecoveredCReferenceRanges::Complete(Vec::new());
}
let mut ranges = Vec::new();
let mut seen = HashSet::default();
let mut stack = vec![(root, root.is_error())];
while let Some((node, inside_error)) = stack.pop() {
let inside_error =
inside_error || node.is_error() || node.kind() == "macro_type_specifier";
if node.kind() == "preproc_arg" {
let macro_value_kind = node.parent().and_then(|parent| {
(parent.child_by_field_name("value") == Some(node)).then_some(parent.kind())
});
if matches!(
macro_value_kind,
Some("preproc_def" | "preproc_function_def")
) {
let name = node_text(node, source);
if !name.is_empty()
&& self.macro_name_may_be_bound_at(file, name, node.start_byte())
&& !push_recovered_c_range(
&mut ranges,
&mut seen,
node.start_byte(),
node.end_byte(),
node,
limit,
)
{
return RecoveredCReferenceRanges::LimitExceeded;
}
}
if macro_value_kind == Some("preproc_def") {
for reference in object_macro_replacement_type_references(node, source) {
for range in reference.component_ranges {
let visible = self
.visible_identifier_candidates(file, &source[range.clone()])
.any(|candidate| candidate.is_class() || candidate.is_module());
if visible
&& !push_recovered_c_range(
&mut ranges,
&mut seen,
range.start,
range.end,
node,
limit,
)
{
return RecoveredCReferenceRanges::LimitExceeded;
}
}
}
}
}
if inside_error
&& recovered_c_reference_node(self, file, node, source)
&& !push_recovered_c_range(
&mut ranges,
&mut seen,
node.start_byte(),
node.end_byte(),
node,
limit,
)
{
return RecoveredCReferenceRanges::LimitExceeded;
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
stack.push((child, inside_error));
}
}
ranges.sort_unstable();
RecoveredCReferenceRanges::Complete(ranges)
}
pub fn macro_target_is_visible_candidate(&self, file: &ProjectFile, target: &CodeUnit) -> bool {
self.visible_identifier_candidates(file, target.identifier())
.filter(|candidate| candidate.is_macro())
.any(|candidate| {
candidate.source() == target.source() && candidate.fq_name() == target.fq_name()
})
}
pub fn object_macro_replacement_at(
&self,
file: &ProjectFile,
name: &str,
before_byte: usize,
) -> Option<String> {
let environment = self.macro_environment(file, before_byte);
let binding = environment.binding(name)?;
if !binding.exact {
return None;
}
match &binding.definition {
MacroDefinition::Object { replacement } => Some(replacement.clone()),
MacroDefinition::Function { .. }
| MacroDefinition::VariadicFunction { .. }
| MacroDefinition::Unsupported => None,
}
}
fn apply_macro_events(
&self,
file: &ProjectFile,
environment: &mut MacroEnvironment,
include_stack: &mut HashSet<ProjectFile>,
) {
if !include_stack.insert(file.clone()) {
return;
}
if self.cpp.prepared_syntax(self.token, file).is_none() {
environment.mark_unknown_names(file, 0);
include_stack.remove(file);
return;
}
match self.macro_include_protection(file) {
MacroIncludeProtection::MacroGuard(guard) => match environment.binding(&guard) {
Some(binding) if binding.is_exact() => {
include_stack.remove(file);
return;
}
Some(_) | None if environment.unknown_names => {
let mut ambiguous_seen = HashSet::default();
self.mark_macro_events_ambiguous(
file,
environment,
&mut ambiguous_seen,
file,
0,
);
include_stack.remove(file);
return;
}
Some(_) => {
let mut ambiguous_seen = HashSet::default();
self.mark_macro_events_ambiguous(
file,
environment,
&mut ambiguous_seen,
file,
0,
);
include_stack.remove(file);
return;
}
None => {}
},
MacroIncludeProtection::PragmaOnce => {
if !environment.applied_pragma_once_files.insert(file.clone()) {
include_stack.remove(file);
return;
}
if environment.maybe_applied_pragma_once_files.remove(file) {
let mut ambiguous_seen = HashSet::default();
environment.applied_pragma_once_files.remove(file);
self.mark_macro_events_ambiguous(
file,
environment,
&mut ambiguous_seen,
file,
0,
);
environment.maybe_applied_pragma_once_files.remove(file);
environment.applied_pragma_once_files.insert(file.clone());
include_stack.remove(file);
return;
}
}
MacroIncludeProtection::None => {}
}
let cell = self.macro_event_cell(file);
let events = cell.get_or_init(|| Arc::from(self.collect_macro_events(file)));
for event in events.iter() {
self.apply_macro_event(file, event, environment, include_stack);
}
include_stack.remove(file);
}
fn apply_macro_event(
&self,
file: &ProjectFile,
event: &MacroEvent,
environment: &mut MacroEnvironment,
include_stack: &mut HashSet<ProjectFile>,
) {
#[cfg(any(test, feature = "test-support"))]
self.macro_event_application_count
.fetch_add(1, Ordering::Relaxed);
match event {
MacroEvent::Define {
name,
binding,
conditionals,
byte,
} => match self.macro_event_condition_value(file, *byte, environment, conditionals) {
Some(true) => environment.insert(name.clone(), binding.clone()),
Some(false) => {}
None => Self::merge_conditional_macro_definition(
environment,
name,
binding,
file,
*byte,
),
},
MacroEvent::Undef {
name,
conditionals,
byte,
} => match self.macro_event_condition_value(file, *byte, environment, conditionals) {
Some(true) => environment.remove(name),
Some(false) => {}
None => {
if environment.binding(name).is_some() {
environment.insert(name.clone(), MacroBinding::ambiguous(file, *byte));
}
}
},
MacroEvent::Include {
targets,
conditionals,
byte,
} => {
let condition =
self.macro_event_condition_value(file, *byte, environment, conditionals);
if condition == Some(false) {
return;
}
if targets.is_empty() {
environment.mark_unknown_names(file, *byte);
return;
}
if condition.is_none() || targets.len() > 1 {
let mut ambiguous_seen = HashSet::default();
for target in targets {
self.mark_macro_events_ambiguous(
target,
environment,
&mut ambiguous_seen,
file,
*byte,
);
}
} else if let Some(target) = targets.first() {
self.apply_macro_events(target, environment, include_stack);
}
}
MacroEvent::Invalidate { byte } => {
for binding in environment.bindings.values_mut() {
*binding = MacroBinding::uncertain_from(binding, file, *byte);
}
}
}
}
fn macro_event_condition_value(
&self,
file: &ProjectFile,
event_byte: usize,
environment: &MacroEnvironment,
conditionals: &OwningPreprocessorConditionals,
) -> Option<bool> {
if conditionals.is_empty() {
return Some(true);
}
let prepared = self.cpp.prepared_syntax(self.token, file)?;
let source = prepared.source();
let root = prepared.tree().root_node();
let descendant = root.descendant_for_byte_range(
event_byte,
event_byte.saturating_add(1).min(source.len()),
)?;
let mut unknown = false;
let mut current = descendant.parent();
while let Some(conditional) = current {
if matches!(
conditional.kind(),
"preproc_if" | "preproc_ifdef" | "preproc_elif"
) && conditionals.contains(&conditional.start_byte())
{
match self.preprocessor_conditional_branch_value(
conditional,
descendant,
source,
environment,
) {
Some(true) => {}
Some(false) => return Some(false),
None => unknown = true,
}
}
current = conditional.parent();
}
(!unknown).then_some(true)
}
fn preprocessor_conditional_branch_value(
&self,
conditional: Node<'_>,
descendant: Node<'_>,
source: &str,
environment: &MacroEnvironment,
) -> Option<bool> {
let value = match conditional.kind() {
"preproc_ifdef" => {
let name = conditional.child_by_field_name("name")?;
let defined = self.macro_name_defined_value(environment, node_text(name, source));
match conditional.child(0)?.kind() {
"#ifdef" => defined,
"#ifndef" => defined.map(|defined| !defined),
_ => None,
}
}
"preproc_if" | "preproc_elif" => conditional
.child_by_field_name("condition")
.and_then(|condition| {
self.preprocessor_integer_value(
condition,
source,
environment,
&mut Vec::new(),
0,
)
})
.map(|value| value != 0),
_ => unreachable!(),
};
match preprocessor_branch_of_descendant(
conditional,
descendant,
&mut PreprocessorContextCache::default(),
) {
PreprocessorBranch::Consequence => value,
PreprocessorBranch::Structured(_) | PreprocessorBranch::DisplacedElse => {
value.map(|value| !value)
}
PreprocessorBranch::Unreadable => None,
}
}
fn macro_name_defined_value(&self, environment: &MacroEnvironment, name: &str) -> Option<bool> {
if environment.known_undefined_names.contains(name) {
return Some(false);
}
if let Some(binding) = environment.binding(name) {
return binding.is_exact().then_some(true);
}
environment
.build_proven_defines
.contains(name)
.then_some(true)
}
fn preprocessor_integer_value(
&self,
expression: Node<'_>,
source: &str,
environment: &MacroEnvironment,
expansion_stack: &mut Vec<(ProjectFile, usize)>,
depth: usize,
) -> Option<i128> {
if depth >= 64 {
return None;
}
match expression.kind() {
"number_literal" => parse_cpp_integer_literal(node_text(expression, source)),
"identifier" | "type_identifier" => {
let binding = environment.binding(node_text(expression, source))?;
if !binding.is_exact() {
return None;
}
let MacroDefinition::Object { replacement } = &binding.definition else {
return None;
};
let identity = (binding.source.clone(), binding.declaration_byte);
if expansion_stack.contains(&identity) {
return None;
}
expansion_stack.push(identity);
let parsed = self.parsed_macro_replacement(binding, replacement);
let value = match parsed.as_ref() {
ParsedMacroReplacement::Parsed {
source: replacement_source,
tree,
} => first_descendant_of_kind(tree.root_node(), "call_expression")
.and_then(|call| call.child_by_field_name("arguments"))
.and_then(|arguments| argument_children(arguments).next())
.and_then(|argument| {
self.preprocessor_integer_value(
argument,
replacement_source,
environment,
expansion_stack,
depth + 1,
)
}),
ParsedMacroReplacement::Unsupported => None,
};
expansion_stack.pop();
value
}
"preproc_defined" => {
let mut cursor = expression.walk();
let name = expression
.named_children(&mut cursor)
.find(|child| child.kind() == "identifier")?;
self.macro_name_defined_value(environment, node_text(name, source))
.map(i128::from)
}
"parenthesized_expression" => expression.named_child(0).and_then(|child| {
self.preprocessor_integer_value(
child,
source,
environment,
expansion_stack,
depth + 1,
)
}),
"unary_expression" => {
let operator = expression.child_by_field_name("operator")?.kind();
let argument = expression.child_by_field_name("argument")?;
let value = self.preprocessor_integer_value(
argument,
source,
environment,
expansion_stack,
depth + 1,
)?;
match operator {
"+" => Some(value),
"-" => value.checked_neg(),
"!" => Some(i128::from(value == 0)),
"~" => Some(!value),
_ => None,
}
}
"binary_expression" => {
let left = self.preprocessor_integer_value(
expression.child_by_field_name("left")?,
source,
environment,
expansion_stack,
depth + 1,
)?;
let right = self.preprocessor_integer_value(
expression.child_by_field_name("right")?,
source,
environment,
expansion_stack,
depth + 1,
)?;
match expression.child_by_field_name("operator")?.kind() {
"+" => left.checked_add(right),
"-" => left.checked_sub(right),
"*" => left.checked_mul(right),
"/" => left.checked_div(right),
"%" => left.checked_rem(right),
"<<" => u32::try_from(right)
.ok()
.and_then(|shift| left.checked_shl(shift)),
">>" => u32::try_from(right)
.ok()
.and_then(|shift| left.checked_shr(shift)),
"<" => Some(i128::from(left < right)),
"<=" => Some(i128::from(left <= right)),
">" => Some(i128::from(left > right)),
">=" => Some(i128::from(left >= right)),
"==" => Some(i128::from(left == right)),
"!=" => Some(i128::from(left != right)),
"&" => Some(left & right),
"|" => Some(left | right),
"^" => Some(left ^ right),
"&&" => Some(i128::from(left != 0 && right != 0)),
"||" => Some(i128::from(left != 0 || right != 0)),
_ => None,
}
}
_ => None,
}
}
fn mark_macro_events_ambiguous(
&self,
file: &ProjectFile,
environment: &mut MacroEnvironment,
include_stack: &mut HashSet<ProjectFile>,
conditional_file: &ProjectFile,
conditional_byte: usize,
) {
if !include_stack.insert(file.clone()) {
return;
}
if self.cpp.prepared_syntax(self.token, file).is_none() {
environment.mark_unknown_names(conditional_file, conditional_byte);
return;
}
match self.macro_include_protection(file) {
MacroIncludeProtection::MacroGuard(guard) => {
if environment
.binding(&guard)
.is_some_and(MacroBinding::is_exact)
{
return;
}
}
MacroIncludeProtection::PragmaOnce => {
if environment.applied_pragma_once_files.contains(file) {
return;
}
environment
.maybe_applied_pragma_once_files
.insert(file.clone());
}
MacroIncludeProtection::None => {}
}
let cell = self.macro_event_cell(file);
let events = cell.get_or_init(|| Arc::from(self.collect_macro_events(file)));
for event in events.iter() {
#[cfg(any(test, feature = "test-support"))]
self.macro_event_application_count
.fetch_add(1, Ordering::Relaxed);
match event {
MacroEvent::Define { name, binding, .. } => {
Self::merge_conditional_macro_definition(
environment,
name,
binding,
conditional_file,
conditional_byte,
);
}
MacroEvent::Undef { name, .. } => {
if environment.binding(name).is_some() {
environment.insert(
name.clone(),
MacroBinding::ambiguous(conditional_file, conditional_byte),
);
} else {
environment.remove_known_undefined(name);
}
}
MacroEvent::Include { targets, .. } => {
if targets.is_empty() {
environment.mark_unknown_names(conditional_file, conditional_byte);
continue;
}
for target in targets {
self.mark_macro_events_ambiguous(
target,
environment,
include_stack,
conditional_file,
conditional_byte,
);
}
}
MacroEvent::Invalidate { .. } => {
for binding in environment.bindings.values_mut() {
*binding = MacroBinding::uncertain_from(
binding,
conditional_file,
conditional_byte,
);
}
}
}
}
}
fn merge_conditional_macro_definition(
environment: &mut MacroEnvironment,
name: &str,
possible_binding: &MacroBinding,
conditional_file: &ProjectFile,
conditional_byte: usize,
) {
if environment.binding(name).is_some_and(|current| {
current.definition != MacroDefinition::Unsupported
&& current.definition == possible_binding.definition
}) {
return;
}
environment.insert(
name.to_string(),
MacroBinding::ambiguous(conditional_file, conditional_byte),
);
}
pub fn macro_include_protection(&self, file: &ProjectFile) -> MacroIncludeProtection {
let value = self
.macro_include_protection_cells
.read(&self.reads.memo_attribution(), |cells, capture| {
let cell = cells
.lock()
.expect("C++ include protection cache poisoned")
.entry(file.clone())
.or_insert_with(|| Arc::new(PoolSafeMemo::new()))
.clone();
Ok::<_, std::convert::Infallible>(cell.get_or_build_pool_independent(|| {
capture.build(|| {
self.cpp.prepared_syntax(self.token, file).map_or(
MacroIncludeProtection::None,
|prepared| {
top_level_macro_include_protection(
prepared.tree().root_node(),
prepared.source(),
)
},
)
})
}))
})
.expect("infallible include protection build");
(*value).clone()
}
fn collect_macro_events(&self, file: &ProjectFile) -> Vec<MacroEvent> {
collect_macro_events(self.cpp, self.token, file)
}
pub fn ordinary_type_import_cell(&self, file: &ProjectFile) -> OrdinaryTypeImportCell {
visibility_memo_value(
&self.ordinary_type_import_cells,
&self.reads,
file.clone(),
|| Arc::new(EffectiveUsingIndex::new(file.clone(), self.reads.clone())),
)
}
pub fn project_using_index(
&self,
build: impl FnOnce() -> ProjectUsingIndex,
) -> Arc<ProjectUsingIndex> {
visibility_once_value(&self.project_using_index, &self.reads, || Arc::new(build()))
}
pub fn all_visible_source_files(&self) -> Vec<ProjectFile> {
let mut files = self
.visible_source_files_by_root
.values()
.flatten()
.cloned()
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
files.sort_by(|left, right| left.rel_path().cmp(right.rel_path()));
files
}
pub(super) fn namespace_alias_cell(
&self,
root: &ProjectFile,
) -> super::namespace_alias::NamespaceAliasCell {
visibility_memo_value(
&self.namespace_alias_sites,
&self.reads,
root.clone(),
|| Arc::new(VisibilityCell::new(self.reads.clone())),
)
}
pub(super) fn visible_source_files(&self, root: &ProjectFile) -> Vec<ProjectFile> {
self.visible_source_files_by_root.get(root).map_or_else(
|| vec![root.clone()],
|files| {
files
.iter()
.chain([root])
.cloned()
.collect::<HashSet<_>>()
.into_iter()
.collect()
},
)
}
pub fn source_is_visible(&self, root: &ProjectFile, source: &ProjectFile) -> bool {
self.visible_source_files_by_root
.get(root)
.is_some_and(|files| files.contains(source))
}
fn visible_parser_alias_name_is_visible(&self, file: &ProjectFile, name: &str) -> bool {
try_visibility_memo_value(
&self.visible_parser_alias_name_sets,
&self.reads,
file.clone(),
|| {
let mut names = HashSet::default();
let visible_files = self
.visible_source_files_by_root
.get(file)
.cloned()
.unwrap_or_else(|| HashSet::from_iter([file.clone()]));
for visible_file in visible_files {
let aliases = self
.canonical_alias_cell(self.cpp, &visible_file)
.ok_or(())?;
for alias in aliases.get().expect("published canonical aliases").iter() {
names.insert(alias.name.clone());
}
}
#[cfg(any(test, feature = "test-support"))]
self.visible_parser_alias_name_set_build_count
.fetch_add(1, Ordering::Relaxed);
Ok::<_, ()>(Arc::new(names))
},
)
.ok()
.is_some_and(|names| names.contains(name))
}
pub fn parser_alias_name_may_resolve_to_target(
&self,
file: &ProjectFile,
alias_name: &str,
target: &CodeUnit,
) -> bool {
let started = std::time::Instant::now();
self.parser_alias_fallback_calls
.fetch_add(1, Ordering::Relaxed);
let matched = visibility_memo_value(
&self.parser_alias_target_matches,
&self.reads,
(
file.clone(),
alias_name.to_string(),
logical_symbol_key(target),
),
|| {
any_visible_source_file_matches(
&self.visible_source_files_by_root,
file,
|visible_file| {
self.parser_alias_fallback_files
.fetch_add(1, Ordering::Relaxed);
self.file_alias_matches(self.cpp, visible_file, alias_name, target)
},
)
},
);
self.parser_alias_fallback_elapsed_micros.fetch_add(
started.elapsed().as_micros().min(usize::MAX as u128) as usize,
Ordering::Relaxed,
);
matched
}
fn callable_arities_for_target(
&self,
analyzer: &CppGraphSource<'_>,
cpp: &dyn CppSource,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
spec: &TargetSpec,
) -> Vec<ActivatedCallableArity> {
let Some(signature) = spec.target.signature() else {
return Vec::new();
};
let Some(candidates) = self
.visible_by_identifier
.get(file)
.and_then(|by_name| by_name.get(&spec.member_name))
else {
return Vec::new();
};
let differing_candidates = candidates
.iter()
.filter(|candidate| {
candidate.is_function()
&& candidate.fq_name() == spec.target.fq_name()
&& candidate.signature() == Some(signature)
})
.filter_map(|candidate| {
analyzer
.signature_metadata(candidate)
.into_iter()
.find_map(|metadata| metadata.callable_arity())
.filter(|arity| Some(*arity) != spec.callable_arity)
.map(|arity| (candidate, arity))
})
.collect::<Vec<_>>();
if differing_candidates.is_empty() {
return Vec::new();
}
let mut arities = Vec::with_capacity(differing_candidates.len());
let effective_guards = |guards: Option<&HashSet<PreprocessorGuard>>| {
self.effective_reference_guards(file, guards)
};
let reference = CallableReferenceContext {
effective_guards: &effective_guards,
file_guards: OnceCell::new(),
file,
position: None,
};
for (candidate, candidate_arity) in differing_candidates {
let declaration_activation =
callable_declaration_activation_in_file(analyzer, candidate, &reference);
let Some(declaration_activation) = declaration_activation else {
continue;
};
let activation_byte = if candidate.source() == file {
Some(declaration_activation)
} else {
self.include_activation_for_source(cpp, file, prepared, candidate.source())
};
if let Some(activation_byte) = activation_byte {
arities.push(ActivatedCallableArity {
activation_byte,
arity: candidate_arity,
});
}
}
arities
}
fn callable_parameter_macro_arity(
&self,
target: &CodeUnit,
parameter_types: Option<&[String]>,
) -> Option<CallableArity> {
let parameter_types = parameter_types?;
let [macro_name] = parameter_types else {
return None;
};
if macro_name.is_empty()
|| !macro_name
.chars()
.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
{
return None;
}
let cache_key = (target.source().clone(), macro_name.clone());
visibility_memo_value(
&self.callable_parameter_macro_arities,
&self.reads,
cache_key,
|| {
let mut visible_files = HashSet::default();
collect_include_closure(
&self.cpp_source(),
&self.cpp.include_target_index(),
target.source(),
&mut visible_files,
None,
);
let mut arities = Vec::new();
for visible_file in visible_files {
let cell = self.macro_event_cell(&visible_file);
for event in cell
.get_or_init(|| Arc::from(self.collect_macro_events(&visible_file)))
.iter()
{
let MacroEvent::Define { name, binding, .. } = event else {
continue;
};
if name != macro_name {
continue;
}
let MacroDefinition::Object { replacement } = &binding.definition else {
continue;
};
let Some(arity) = parse_macro_parameter_list_arity(replacement) else {
continue;
};
if !arities.contains(&arity) {
arities.push(arity);
}
}
}
let required = arities
.iter()
.filter_map(|arity| (0..=arity.total()).find(|count| arity.accepts(*count)))
.min()?;
let total = arities.iter().map(|arity| arity.total()).max()?;
let repeated = arities
.iter()
.any(|arity| arity.accepts(arity.total().saturating_add(1)));
Some(CallableArity::new(required, total, repeated))
},
)
}
pub fn include_activation_for_source(
&self,
cpp: &dyn CppSource,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
donor_source: &ProjectFile,
) -> Option<usize> {
visibility_memo_value(
&self.include_activation_cells,
&self.reads,
(file.clone(), donor_source.clone()),
|| {
#[cfg(any(test, feature = "test-support"))]
self.include_activation_build_count
.fetch_add(1, Ordering::Relaxed);
let effective = OnceCell::new();
find_include_activation(cpp, self.token, file, prepared, donor_source, &|| {
effective
.get_or_init(|| {
self.effective_reference_guards(file, Some(&HashSet::default()))
})
.as_ref()
})
},
)
}
pub fn conditional_include_projections_for_source(
&self,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
donor_source: &ProjectFile,
) -> Arc<[ConditionalIncludeProjection]> {
static EMPTY: OnceLock<Arc<[ConditionalIncludeProjection]>> = OnceLock::new();
let index = visibility_memo_value(
&self.conditional_include_projection_cells,
&self.reads,
file.clone(),
|| {
#[cfg(any(test, feature = "test-support"))]
self.conditional_include_projection_index_build_count
.fetch_add(1, Ordering::Relaxed);
let effective = OnceCell::new();
Arc::new(find_conditional_include_projection_index(
self.cpp,
self.token,
file,
prepared,
&|| {
effective
.get_or_init(|| {
self.effective_reference_guards(file, Some(&HashSet::default()))
})
.as_ref()
},
&|| {
#[cfg(any(test, feature = "test-support"))]
self.conditional_include_projection_state_count
.fetch_add(1, Ordering::Relaxed);
},
))
},
);
index
.get(donor_source)
.cloned()
.unwrap_or_else(|| Arc::clone(EMPTY.get_or_init(|| Arc::from([]))))
}
fn include_language_guard_is_stable(&self, file: &ProjectFile) -> bool {
let mut macro_events = |file: &ProjectFile| {
let cell = self.macro_event_cell(file);
cell.get_or_init(|| Arc::from(self.collect_macro_events(file)))
.as_ref()
.to_vec()
.into_boxed_slice()
};
include_language_guard_is_stable_with(
self.cpp,
self.token,
file,
&mut macro_events,
&mut |file: &ProjectFile, prepared: &PreparedSyntaxTree| {
self.structured_include_facts(file, prepared)
},
)
}
#[cfg(any(test, feature = "test-support"))]
pub fn conditional_include_projection_work_counts_for_test(&self) -> (usize, usize) {
(
self.conditional_include_projection_index_build_count
.load(Ordering::Relaxed),
self.conditional_include_projection_state_count
.load(Ordering::Relaxed),
)
}
#[cfg(any(test, feature = "test-support"))]
pub fn conditional_include_target_state_count_for_test(&self) -> usize {
self.conditional_include_target_state_count
.load(Ordering::Relaxed)
}
#[cfg(any(test, feature = "test-support"))]
pub fn include_activation_build_count_for_test(&self) -> usize {
self.include_activation_build_count.load(Ordering::Relaxed)
}
#[cfg(any(test, feature = "test-support"))]
pub fn note_using_donor_activation_for_test(&self) {
self.using_donor_activation_count
.fetch_add(1, Ordering::Relaxed);
}
#[cfg(not(any(test, feature = "test-support")))]
pub fn note_using_donor_activation_for_test(&self) {}
#[cfg(any(test, feature = "test-support"))]
pub fn note_using_namespace_lookup_for_test(&self) {
self.using_namespace_lookup_count
.fetch_add(1, Ordering::Relaxed);
}
#[cfg(not(any(test, feature = "test-support")))]
pub fn note_using_namespace_lookup_for_test(&self) {}
#[cfg(any(test, feature = "test-support"))]
pub fn note_using_name_candidate_inspection_for_test(&self) {
self.using_name_candidate_inspection_count
.fetch_add(1, Ordering::Relaxed);
}
#[cfg(not(any(test, feature = "test-support")))]
pub fn note_using_name_candidate_inspection_for_test(&self) {}
#[cfg(any(test, feature = "test-support"))]
pub fn using_work_counts_for_test(&self) -> (usize, usize, usize, usize) {
(
self.using_donor_activation_count.load(Ordering::Relaxed),
self.using_namespace_lookup_count.load(Ordering::Relaxed),
self.callable_reference_spec_build_count
.load(Ordering::Relaxed),
self.using_name_candidate_inspection_count
.load(Ordering::Relaxed),
)
}
pub fn is_physically_visible(&self, file: &ProjectFile, target: &CodeUnit) -> bool {
file == target.source()
|| self
.visible_by_file
.get(file)
.is_some_and(|visible| visible.contains(target))
}
pub fn declaration_visible_at(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
declaration: &CodeUnit,
reference_byte: usize,
) -> bool {
let reference_guards = OnceCell::new();
if declaration.is_callable() && !callable_target_has_definition_body(analyzer, declaration)
{
return self.physical_declaration_visible_at(
analyzer,
file,
declaration,
reference_byte,
&reference_guards,
);
}
self.visible_identifier_candidates(file, declaration.identifier())
.filter(|candidate| {
(if declaration.is_callable()
&& reference_uses_c_semantics(self.cpp, file)
!= reference_uses_c_semantics(self.cpp, declaration.source())
{
crate::identity::cpp_callable_definitions_share_identity_evidence_with_visibility(
analyzer,
self,
candidate,
declaration,
|left, right| {
crate::identity::cpp_header_body_files_are_related(
self.cpp, self.token, left, right,
)
},
)
} else {
self.same_logical_callable(analyzer, candidate, declaration)
})
|| flattened_macro_namespace_declaration_matches(
analyzer,
self.cpp,
file,
candidate,
declaration,
reference_byte,
)
})
.any(|candidate| {
self.physical_declaration_visible_at(
analyzer,
file,
candidate,
reference_byte,
&reference_guards,
)
})
}
pub fn declaration_guards_compatible_at_reference(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
declaration: &CodeUnit,
reference: Node<'_>,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let reference_guards = preprocessor_guard_environment(reference, prepared.source());
let requirements = declaration_guard_requirements(analyzer, self.cpp, declaration);
if reference_guards.as_ref().is_some_and(HashSet::is_empty)
&& requirements.iter().any(|(_, required)| required.is_empty())
{
return true;
}
let reference_guards = self.effective_reference_guards(file, reference_guards.as_ref());
requirements.iter().any(|(_, required)| {
guards_compatible_at_reference(required, reference_guards.as_ref())
})
}
pub fn declaration_guards_implied_at_reference(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
declaration: &CodeUnit,
reference: Node<'_>,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let raw = preprocessor_guard_environment(reference, prepared.source());
let requirements = declaration_guard_requirements(analyzer, self.cpp, declaration);
if raw.is_some() && requirements.iter().any(|(_, required)| required.is_empty()) {
return true;
}
let effective = self.effective_reference_guards(file, raw.as_ref());
requirements.iter().any(|(byte, required)| {
if guard_requirements_hold_at_reference(required, effective.as_ref())
&& (declaration.source() != file
|| self.preprocessor_guards_stable_between(
file,
(*byte).min(reference.start_byte()),
(*byte).max(reference.start_byte()),
required,
))
{
return true;
}
if declaration.source() != file {
return false;
}
let root = prepared.tree().root_node();
let Some(node) = root.descendant_for_byte_range(
*byte,
byte.saturating_add(1).min(prepared.source().len()),
) else {
return false;
};
let conditionals = owning_preprocessor_conditionals(root, node, prepared.source());
if conditionals.is_empty() {
return false;
}
let mut pending = conditionals.iter().copied().peekable();
let mut ancestor = node.parent();
let mut evaluated = false;
while let Some(conditional) = ancestor {
if pending.peek() == Some(&conditional.start_byte()) {
pending.next();
match conditional.kind() {
"preproc_if" | "preproc_ifdef" | "preproc_elif" => {
let environment =
self.macro_environment(file, conditional.start_byte());
if self.preprocessor_conditional_branch_value(
conditional,
node,
prepared.source(),
&environment,
) != Some(true)
{
return false;
}
evaluated = true;
}
"preproc_else" => {}
_ => return false,
}
}
ancestor = conditional.parent();
}
evaluated && pending.peek().is_none()
})
}
pub fn declaration_visible_at_reference(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
declaration: &CodeUnit,
reference: Node<'_>,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
if !self.declaration_guards_compatible_at_reference(analyzer, file, declaration, reference)
{
return false;
}
if declaration.source() == file
&& !analyzer.reference_uses_c_semantics(file)
&& (declaration.is_field() || declaration.is_callable())
&& type_owner_of(analyzer, declaration).is_some_and(|owner| {
self.indexed_enclosing_owner_scope(analyzer, file, reference)
.is_some_and(|scope| scope == canonical_cpp_scope_components(&owner))
})
{
return true;
}
if declaration.is_field() && declaration.source() == file {
let reference_byte = reference.start_byte();
let reference_function =
real_function_definition_ancestor(reference, prepared.source());
let guards = OnceCell::new();
let effective_guards = |guards: Option<&HashSet<PreprocessorGuard>>| {
self.effective_reference_guards(file, guards)
};
let field_reference = CallableReferenceContext {
effective_guards: &effective_guards,
file_guards: OnceCell::new(),
file,
position: Some(CallableReferencePosition {
prepared: prepared.as_ref(),
byte: reference_byte,
guards: &guards,
}),
};
let mut has_local_declaration = false;
let mut local_declaration_visible = false;
for declaration in callable_declaration_nodes(analyzer, prepared.as_ref(), declaration)
{
let Some(declaration_function) =
real_function_definition_ancestor(declaration, prepared.source())
else {
continue;
};
has_local_declaration = true;
if reference_function.is_some_and(|reference_function| {
reference_function.start_byte() == declaration_function.start_byte()
&& reference_function.end_byte() == declaration_function.end_byte()
}) && callable_preprocessor_context_is_visible_for_reference(
declaration,
prepared.source(),
&field_reference,
) && declaration.end_byte() < reference_byte
{
local_declaration_visible = true;
break;
}
}
if has_local_declaration {
return local_declaration_visible;
}
}
let guards = OnceCell::new();
self.physical_declaration_visible_at(
analyzer,
file,
declaration,
reference.start_byte(),
&guards,
)
}
pub fn declaration_visible_for_c_forward_call(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
declaration: &CodeUnit,
reference_byte: usize,
) -> bool {
if self.declaration_visible_at(analyzer, file, declaration, reference_byte) {
return true;
}
if declaration.source() != file {
return false;
}
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let reference_guards = prepared
.tree()
.root_node()
.descendant_for_byte_range(reference_byte, reference_byte)
.and_then(|node| preprocessor_guard_environment(node, prepared.source()));
declaration_guard_requirements(analyzer, self.cpp, declaration)
.into_iter()
.any(|(_, required)| {
guard_requirements_hold_at_reference(&required, reference_guards.as_ref())
})
}
pub fn callable_arity_at_reference(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidate: &CodeUnit,
reference_byte: usize,
) -> Option<CallableArity> {
let spec = visibility_memo_value(
&self.callable_reference_specs,
&self.reads,
(file.clone(), logical_symbol_key(candidate)),
|| {
let prepared = self.cpp.prepared_syntax(self.token, file)?;
let spec = TargetSpec::from_target(analyzer, candidate)?;
let spec = spec
.with_visible_callable_arities(
analyzer,
self.cpp,
self,
file,
prepared.as_ref(),
)
.into_owned();
#[cfg(any(test, feature = "test-support"))]
self.callable_reference_spec_build_count
.fetch_add(1, Ordering::Relaxed);
Some(Arc::new(spec))
},
);
spec.as_ref()?.callable_arity_at(reference_byte)
}
fn physical_declaration_visible_at(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
declaration: &CodeUnit,
reference_byte: usize,
reference_guards: &OnceCell<Option<HashSet<PreprocessorGuard>>>,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let effective_guards = |guards: Option<&HashSet<PreprocessorGuard>>| {
self.effective_reference_guards(file, guards)
};
let reference = CallableReferenceContext {
effective_guards: &effective_guards,
file_guards: OnceCell::new(),
file,
position: Some(CallableReferencePosition {
prepared: prepared.as_ref(),
byte: reference_byte,
guards: reference_guards,
}),
};
if declaration.source() == file {
return callable_declaration_activation_in_file(analyzer, declaration, &reference)
.or_else(|| {
self.exhaustive_guard_family_activation(analyzer, declaration, &reference)
})
.is_some_and(|activation| activation < reference_byte);
}
if self
.exhaustive_guard_family_activation(analyzer, declaration, &reference)
.is_some()
{
return self.foreign_declaration_reachable_at_reference(
file,
prepared.as_ref(),
declaration.source(),
&HashSet::default(),
reference.guards(),
reference_byte,
);
}
if !self.foreign_callable_declaration_is_active(analyzer, declaration, &reference) {
return false;
}
declaration_guard_requirements(analyzer, self.cpp, declaration)
.into_iter()
.any(|(_, declaration_guards)| {
self.foreign_declaration_reachable_at_reference(
file,
prepared.as_ref(),
declaration.source(),
&declaration_guards,
reference.guards(),
reference_byte,
)
})
}
fn foreign_callable_declaration_is_active(
&self,
analyzer: &CppGraphSource<'_>,
declaration: &CodeUnit,
reference: &CallableReferenceContext<'_>,
) -> bool {
let build_decides = !self.compile_context_is_absent(reference.file);
let active = reference.guards();
analyzer
.declaration_source_occurrences(declaration)
.into_iter()
.flatten()
.any(|occurrence| {
if occurrence.fact.callable_activation.is_none() {
return false;
}
let Some(required) = occurrence
.fact
.callable_guards
.as_ref()
.and_then(cpp_guard_set_to_runtime)
else {
return false;
};
required.is_empty()
|| if build_decides {
guard_requirements_hold_at_reference(&required, active)
} else {
guards_compatible_at_reference(&required, active)
}
})
}
pub fn external_type_candidate_visible_at(
&self,
file: &ProjectFile,
candidate: &CodeUnit,
reference_byte: usize,
) -> bool {
if candidate.source() == file {
return true;
}
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
self.visible_identifier_candidates(file, candidate.identifier())
.filter(|peer| same_logical_symbol(candidate, peer))
.any(|peer| {
peer.source() == file
|| self
.include_activation_for_source(
self.cpp,
file,
prepared.as_ref(),
peer.source(),
)
.is_some_and(|activation| activation <= reference_byte)
})
}
pub fn external_type_declaration_visible_at(
&self,
file: &ProjectFile,
candidate: &CodeUnit,
reference_byte: usize,
) -> bool {
if candidate.source() == file {
return true;
}
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
self.include_activation_for_source(self.cpp, file, prepared.as_ref(), candidate.source())
.is_some_and(|activation| activation <= reference_byte)
}
pub fn compile_proven_guards(&self, file: &ProjectFile) -> Arc<HashSet<PreprocessorGuard>> {
visibility_memo_value(
&self.compile_proven_guard_cells,
&self.reads,
file.clone(),
|| Arc::new(compile_proven_guard_facts(self.cpp, file)),
)
}
fn include_path_admission(&self, file: &ProjectFile) -> IncludePathAdmission {
visibility_memo_value(
&self.include_path_admission_cells,
&self.reads,
file.clone(),
|| {
if self.compile_context_is_absent(file) {
IncludePathAdmission::Compatible
} else {
IncludePathAdmission::Proven
}
},
)
}
fn compile_context_is_absent(&self, file: &ProjectFile) -> bool {
if !self.cpp.compile_contexts_for(file).is_empty() {
return false;
}
let translation_units = self.cpp.reaching_translation_units(file);
translation_units.is_empty()
|| translation_units
.iter()
.any(|translation_unit| self.cpp.compile_contexts_for(translation_unit).is_empty())
}
pub fn miss_requires_compile_context(
&self,
file: &ProjectFile,
identifier: &str,
reference: Node<'_>,
) -> bool {
if !self.compile_context_is_absent(file) {
return false;
}
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let reference_guards = preprocessor_guard_environment(reference, prepared.source());
let reference_byte = reference.start_byte();
let mut sources = self
.visible_identifier_candidates(file, identifier)
.map(CodeUnit::source)
.filter(|source| *source != file)
.collect::<Vec<_>>();
sources.sort();
sources.dedup();
sources.into_iter().any(|declaration_source| {
self.conditional_include_projections_for_source(
file,
prepared.as_ref(),
declaration_source,
)
.iter()
.any(|projection| {
projection.activation_byte <= reference_byte
&& !guard_requirements_hold_at_reference(
&projection.required_guards,
reference_guards.as_ref(),
)
&& guards_compatible_at_reference(
&projection.required_guards,
reference_guards.as_ref(),
)
})
})
}
fn effective_reference_guards(
&self,
file: &ProjectFile,
reference_guards: Option<&HashSet<PreprocessorGuard>>,
) -> Option<HashSet<PreprocessorGuard>> {
effective_reference_guards(
self.cpp,
file,
reference_guards,
&self.compile_proven_guards(file),
&|| self.include_language_guard_is_stable(file),
)
}
fn foreign_declaration_reachable_at_reference(
&self,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
declaration_source: &ProjectFile,
declaration_guards: &HashSet<PreprocessorGuard>,
reference_guards: Option<&HashSet<PreprocessorGuard>>,
reference_byte: usize,
) -> bool {
let effective = self.effective_reference_guards(file, reference_guards);
let reference_guards = effective.as_ref();
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some() {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=foreign_guard_compatibility declaration_source={} declaration_guards={declaration_guards:?} reference_guards={reference_guards:?}",
declaration_source.rel_path().display(),
);
}
if !guard_requirements_hold_at_reference(declaration_guards, reference_guards) {
return false;
}
if self
.include_activation_for_source(self.cpp, file, prepared, declaration_source)
.is_some_and(|activation| activation <= reference_byte)
{
return true;
}
let projections =
self.conditional_include_projections_for_source(file, prepared, declaration_source);
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some() {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=filtered_projection source={} declaration_guards={} proven_guards={} projections={}",
declaration_source.rel_path().display(),
declaration_guards.len(),
self.compile_proven_guards(file).len(),
projections.len(),
);
}
let admission = IncludePathAdmission::Proven;
projections.iter().any(|projection| {
projection.activation_byte <= reference_byte
&& admission.admits(
&projection.required_guards,
&projection.partial_guards,
reference_guards,
)
&& self.preprocessor_guards_stable_between(
file,
projection.activation_byte,
reference_byte,
&projection.required_guards,
)
})
}
fn foreign_declaration_may_be_reachable_from_raw_guards(
&self,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
declaration_source: &ProjectFile,
declaration_guards: &HashSet<PreprocessorGuard>,
reference_guards: Option<&HashSet<PreprocessorGuard>>,
reference_byte: usize,
) -> bool {
let proven = self.compile_proven_guards(file);
let augmented;
let reference_guards = match reference_guards {
Some(active) if !proven.is_empty() => {
augmented = active.union(&proven).cloned().collect();
Some(&augmented)
}
other => other,
};
if !guards_compatible_at_reference(declaration_guards, reference_guards) {
return false;
}
if self
.include_activation_for_source(self.cpp, file, prepared, declaration_source)
.is_some_and(|activation| activation <= reference_byte)
{
return true;
}
let effective = OnceCell::new();
let reachable = find_conditional_include_projection_for_source(
self.cpp,
self.token,
file,
prepared,
declaration_source,
self.include_path_admission(file),
reference_guards,
reference_byte,
&|| {
effective
.get_or_init(|| {
self.effective_reference_guards(file, Some(&HashSet::default()))
})
.as_ref()
},
&|| {
#[cfg(any(test, feature = "test-support"))]
self.conditional_include_target_state_count
.fetch_add(1, Ordering::Relaxed);
},
);
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some() {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=raw_projection source={} declaration_guards={} proven_guards={} raw_guards={} reachable={reachable}",
declaration_source.rel_path().display(),
declaration_guards.len(),
proven.len(),
reference_guards.map_or(0, HashSet::len),
);
}
reachable
}
fn foreign_declaration_reachable_from_compile_proven_guards(
&self,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
declaration_source: &ProjectFile,
declaration_guards: &HashSet<PreprocessorGuard>,
reference_byte: usize,
) -> bool {
let proven = self.compile_proven_guards(file);
if proven.is_empty()
|| !guard_requirements_hold_at_reference(declaration_guards, Some(proven.as_ref()))
{
return false;
}
let projections =
self.conditional_include_projections_for_source(file, prepared, declaration_source);
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some() {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=compile_proven_projection source={} declaration_guards={} proven_guards={} projections={}",
declaration_source.rel_path().display(),
declaration_guards.len(),
proven.len(),
projections.len(),
);
}
projections.iter().any(|projection| {
projection.activation_byte <= reference_byte
&& guard_requirements_hold_at_reference(
&projection.required_guards,
Some(proven.as_ref()),
)
&& self.preprocessor_guards_stable_between(
file,
0,
projection.activation_byte,
&projection.required_guards,
)
})
}
fn declaration_site_visible_without_reference_environment(
&self,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
site_source: &ProjectFile,
site_byte: usize,
site_guards: &HashSet<PreprocessorGuard>,
reference_byte: usize,
) -> bool {
let report_stats = std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some();
if site_source == file {
let visible = site_byte < reference_byte && site_guards.is_empty();
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=direct_peer source={} declaration_guards={} same_file=true visible={visible}",
site_source.rel_path().display(),
site_guards.len(),
);
}
return visible;
}
let direct = site_guards.is_empty()
&& self
.include_activation_for_source(self.cpp, file, prepared, site_source)
.is_some_and(|activation| activation <= reference_byte);
let compile_proven = !direct
&& self.foreign_declaration_reachable_from_compile_proven_guards(
file,
prepared,
site_source,
site_guards,
reference_byte,
);
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=direct_peer source={} declaration_guards={} same_file=false direct={direct} compile_proven={compile_proven}",
site_source.rel_path().display(),
site_guards.len(),
);
}
direct || compile_proven
}
#[allow(clippy::too_many_arguments)]
fn declaration_site_visible_at_reference<'g>(
&self,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
site_source: &ProjectFile,
site: &DeclarationGuardSite,
reference_byte: usize,
raw_reference_guards: Option<&HashSet<PreprocessorGuard>>,
reference_guards_at_site: impl Fn() -> Option<&'g HashSet<PreprocessorGuard>>,
) -> bool {
if site_source == file {
if site.byte >= reference_byte {
return false;
}
let active = |guards: Option<&HashSet<PreprocessorGuard>>| {
guard_requirements_hold_at_reference(&site.guards, guards)
|| (site.completed_family_before_reference
&& guards_compatible_at_reference(&site.guards, guards))
};
let effective_guards = |guards: Option<&HashSet<PreprocessorGuard>>| {
self.effective_reference_guards(file, guards)
};
let raw_reference_guards = effective_guards(raw_reference_guards);
if !active(raw_reference_guards.as_ref()) {
return false;
}
let reference_guards_at_site = effective_guards(reference_guards_at_site());
return active(reference_guards_at_site.as_ref())
&& self.preprocessor_guards_stable_between(
file,
site.byte,
reference_byte,
&site.guards,
);
}
let raw_feasible = self.foreign_declaration_may_be_reachable_from_raw_guards(
file,
prepared,
site_source,
&site.guards,
raw_reference_guards,
reference_byte,
);
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some() {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=raw_feasibility source={} declaration_guards={} feasible={raw_feasible}",
site_source.rel_path().display(),
site.guards.len(),
);
}
raw_feasible
&& self.foreign_declaration_reachable_at_reference(
file,
prepared,
site_source,
&site.guards,
reference_guards_at_site(),
reference_byte,
)
}
pub fn binding_site_visible_at_reference(
&self,
file: &ProjectFile,
site_source: &ProjectFile,
site_byte: usize,
site_guards: &HashSet<PreprocessorGuard>,
reference: Node<'_>,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
if self.declaration_site_visible_without_reference_environment(
file,
prepared.as_ref(),
site_source,
site_byte,
site_guards,
reference.start_byte(),
) {
return true;
}
let raw_reference_guards = preprocessor_guard_environment(reference, prepared.source());
let reference_guards = OnceCell::new();
self.declaration_site_visible_at_reference(
file,
prepared.as_ref(),
site_source,
&DeclarationGuardSite {
byte: site_byte,
guards: site_guards.clone(),
completed_family_before_reference: false,
},
reference.start_byte(),
raw_reference_guards.as_ref(),
|| {
reference_guards
.get_or_init(|| {
let environment = self.macro_environment(file, reference.start_byte());
raw_reference_guards
.clone()
.filter(|guards| environment.guard_requirements_may_hold(guards))
})
.as_ref()
},
)
}
pub fn external_type_candidate_visible_in_context(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidate: &CodeUnit,
reference: Node<'_>,
) -> bool {
let report_stats = std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some();
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=candidate status=started fqn={} candidate_source={} reference_file={} reference_byte={}",
candidate.fq_name(),
candidate.source().rel_path().display(),
file.rel_path().display(),
reference.start_byte(),
);
}
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let raw_reference_guards = preprocessor_guard_environment(reference, prepared.source());
let reference_guards = OnceCell::new();
let reference_guards_at_site = || {
reference_guards.get_or_init(|| {
let started = Instant::now();
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=macro_environment status=started file={} reference_byte={} raw_guards={}",
file.rel_path().display(),
reference.start_byte(),
raw_reference_guards.as_ref().map_or(0, HashSet::len),
);
}
let macro_environment = self.macro_environment(file, reference.start_byte());
let filtered = raw_reference_guards
.clone()
.filter(|guards| macro_environment.guard_requirements_may_hold(guards));
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=macro_environment status=completed retained={} elapsed_ms={}",
filtered.is_some(),
started.elapsed().as_millis(),
);
}
filtered
})
};
let peers = self
.visible_identifier_candidates(file, candidate.identifier())
.filter(|peer| same_logical_symbol(candidate, peer))
.collect::<Vec<_>>();
if report_stats {
let peer_sources = peers
.iter()
.map(|peer| peer.source().rel_path().display().to_string())
.collect::<Vec<_>>();
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=peers fqn={} sources={peer_sources:?}",
candidate.fq_name(),
);
}
let directly_visible_without_reference_environment = peers.iter().any(|peer| {
declaration_guard_requirements(analyzer, self.cpp, peer)
.into_iter()
.any(|(declaration_byte, declaration_guards)| {
self.declaration_site_visible_without_reference_environment(
file,
prepared.as_ref(),
peer.source(),
declaration_byte,
&declaration_guards,
reference.start_byte(),
)
})
});
if directly_visible_without_reference_environment {
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=candidate status=completed outcome=direct_or_compile_proven fqn={}",
candidate.fq_name(),
);
}
return true;
}
let directly_visible = peers.iter().any(|peer| {
declaration_guard_sites(analyzer, self.cpp, peer, reference.start_byte())
.into_iter()
.any(|site| {
self.declaration_site_visible_at_reference(
file,
prepared.as_ref(),
peer.source(),
&site,
reference.start_byte(),
raw_reference_guards.as_ref(),
|| reference_guards_at_site().as_ref(),
)
})
});
if directly_visible {
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=candidate status=completed outcome=filtered_reference fqn={}",
candidate.fq_name(),
);
}
return true;
}
let complementary = self
.visible_identifier_candidates(file, candidate.identifier())
.filter(|peer| {
peer.kind() == candidate.kind()
&& peer.fq_name() == candidate.fq_name()
&& peer.source() == candidate.source()
})
.collect::<Vec<_>>();
let complementary_family =
self.complementary_same_fqn_type_declarations(analyzer, &complementary, candidate);
let raw_candidate_branch_compatible = complementary_family
&& raw_reference_guards.as_ref().is_some_and(|active| {
declaration_guard_requirements(analyzer, self.cpp, candidate)
.iter()
.any(|(_, required)| merge_preprocessor_guards(required, active).is_some())
});
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=complementary fqn={} candidates={} family={} raw_compatible={}",
candidate.fq_name(),
complementary.len(),
complementary_family,
raw_candidate_branch_compatible,
);
}
let candidate_branch_compatible = raw_candidate_branch_compatible
&& reference_guards_at_site().as_ref().is_some_and(|active| {
declaration_guard_requirements(analyzer, self.cpp, candidate)
.iter()
.any(|(_, required)| merge_preprocessor_guards(required, active).is_some())
});
let complementary_visible = candidate_branch_compatible
&& if candidate.source() == file {
declaration_guard_requirements(analyzer, self.cpp, candidate)
.iter()
.any(|(declaration_byte, _)| *declaration_byte < reference.start_byte())
} else {
self.include_activation_for_source(
self.cpp,
file,
prepared.as_ref(),
candidate.source(),
)
.is_some_and(|activation| activation <= reference.start_byte())
};
if report_stats {
eprintln!(
"BIFROST_CPP_TYPE_VISIBILITY_STATS phase=candidate status=completed outcome={} fqn={}",
if complementary_visible {
"complementary"
} else {
"missing"
},
candidate.fq_name(),
);
}
complementary_visible
}
pub fn is_exhaustive_same_fqn_type_declaration_family(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidate: &CodeUnit,
) -> bool {
let candidates = self
.visible_identifier_candidates(file, candidate.identifier())
.filter(|peer| {
peer.kind() == candidate.kind()
&& peer.fq_name() == candidate.fq_name()
&& peer.source() == candidate.source()
})
.collect::<Vec<_>>();
self.complementary_same_fqn_type_declarations(analyzer, &candidates, candidate)
}
pub fn dependent_member_pointer_alias_visible_in_context(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidate: &CodeUnit,
owner_components: &[String],
reference: Node<'_>,
) -> bool {
if !analyzer
.type_alias_provider()
.is_some_and(|provider| provider.is_type_alias(candidate))
{
return false;
}
let Some((terminal, owner_prefix)) = owner_components.split_last() else {
return false;
};
if terminal != candidate.identifier()
|| canonical_cpp_scope_components(candidate) != owner_components
{
return false;
}
let Some(expected_parent_fq_name) =
brokk_bifrost_core::analyzer::default_parent_fq_name(candidate)
else {
return false;
};
let Some(parent_anchor) = type_owner_of(analyzer, candidate) else {
return false;
};
if parent_anchor.fq_name() != expected_parent_fq_name.as_str()
|| parent_anchor.source() != candidate.source()
|| canonical_cpp_scope_components(&parent_anchor) != owner_prefix
{
return false;
}
if !self.external_type_candidate_visible_at(file, candidate, reference.start_byte())
|| candidate.source() == file
&& !analyzer
.ranges(candidate)
.iter()
.any(|range| range.start_byte < reference.start_byte())
{
return false;
}
let candidate_guards = declaration_guard_requirements(analyzer, self.cpp, candidate);
if candidate_guards.is_empty() {
return false;
}
let same_guard_sets =
|left: &[(usize, HashSet<PreprocessorGuard>)],
right: &[(usize, HashSet<PreprocessorGuard>)]| {
left.iter().all(|(_, left_guards)| {
right
.iter()
.any(|(_, right_guards)| left_guards == right_guards)
})
};
let parent_candidates = self
.visible_identifier_candidates(file, parent_anchor.identifier())
.filter(|peer| {
peer.kind() == parent_anchor.kind()
&& peer.fq_name() == expected_parent_fq_name.as_str()
&& peer.source() == parent_anchor.source()
&& canonical_cpp_scope_components(peer) == owner_prefix
})
.filter_map(|peer| {
let parent_guards = declaration_guard_requirements(analyzer, self.cpp, peer);
(candidate_guards.len() == parent_guards.len()
&& same_guard_sets(&candidate_guards, &parent_guards)
&& same_guard_sets(&parent_guards, &candidate_guards))
.then(|| (peer.clone(), parent_guards))
})
.collect::<Vec<_>>();
let [(parent, _parent_guards)] = parent_candidates.as_slice() else {
return false;
};
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let raw = preprocessor_guard_environment(reference, prepared.source());
let Some(reference_guards) = self.effective_reference_guards(file, raw.as_ref()) else {
return false;
};
if !candidate_guards.iter().any(|(_, target_guards)| {
guards_compatible_at_reference(target_guards, Some(&reference_guards))
&& (candidate.source() != file
|| self.preprocessor_guards_stable_between(
file,
0,
reference.start_byte(),
target_guards,
))
}) {
return false;
}
self.external_type_candidate_visible_in_context(analyzer, file, parent, reference)
}
pub fn external_type_candidate_guard_compatible_in_context(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidate: &CodeUnit,
reference: Node<'_>,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let reference_guards = preprocessor_guard_environment(reference, prepared.source());
self.visible_identifier_candidates(file, candidate.identifier())
.filter(|peer| same_logical_symbol(candidate, peer))
.any(|peer| {
declaration_guard_requirements(analyzer, self.cpp, peer)
.into_iter()
.any(|(declaration_byte, declaration_guards)| {
if peer.source() == file {
let (start, end) = if declaration_byte <= reference.start_byte() {
(declaration_byte, reference.start_byte())
} else {
(reference.start_byte(), declaration_byte)
};
return guard_requirements_hold_at_reference(
&declaration_guards,
reference_guards.as_ref(),
) && self.preprocessor_guards_stable_between(
file,
start,
end,
&declaration_guards,
);
}
self.foreign_declaration_reachable_at_reference(
file,
prepared.as_ref(),
peer.source(),
&declaration_guards,
reference_guards.as_ref(),
reference.start_byte(),
)
})
})
}
pub fn same_file_callable_guard_compatible_ignoring_order(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidate: &CodeUnit,
reference: Node<'_>,
) -> bool {
if candidate.source() != file || !candidate.is_callable() {
return false;
}
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let guards = OnceCell::new();
let effective_guards = |guards: Option<&HashSet<PreprocessorGuard>>| {
self.effective_reference_guards(file, guards)
};
let context = CallableReferenceContext {
effective_guards: &effective_guards,
file_guards: OnceCell::new(),
file,
position: Some(CallableReferencePosition {
prepared: prepared.as_ref(),
byte: reference.start_byte(),
guards: &guards,
}),
};
analyzer
.declaration_source_occurrences(candidate)
.into_iter()
.flatten()
.filter_map(|occurrence| {
let guards = cpp_guard_set_to_runtime(occurrence.fact.callable_guards.as_ref()?)?;
Some(callable_guard_requirements_are_visible(
&guards,
occurrence.fact.callable_guard_completion_byte,
&context,
))
})
.any(|visible| visible)
}
pub fn type_candidate_may_be_visible_before_reference(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidate: &CodeUnit,
reference_byte: usize,
) -> bool {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return false;
};
let root = prepared.tree().root_node();
let end_byte = reference_byte
.saturating_add(1)
.min(prepared.source().len());
let Some(reference) = root.descendant_for_byte_range(reference_byte, end_byte) else {
return false;
};
self.external_type_candidate_visible_in_context(analyzer, file, candidate, reference)
}
pub fn preprocessor_guards_stable_between(
&self,
file: &ProjectFile,
start_byte: usize,
end_byte: usize,
guards: &HashSet<PreprocessorGuard>,
) -> bool {
if guards.is_empty() || start_byte >= end_byte {
return true;
}
let cell = self.macro_event_cell(file);
let events = cell.get_or_init(|| Arc::from(self.collect_macro_events(file)));
let mut visited = HashSet::from_iter([file.clone()]);
!events.iter().any(|event| {
event.byte() >= start_byte
&& event.byte() < end_byte
&& self.macro_event_may_mutate_guards(event, guards, &mut visited)
})
}
fn macro_event_may_mutate_guards(
&self,
event: &MacroEvent,
guards: &HashSet<PreprocessorGuard>,
visited: &mut HashSet<ProjectFile>,
) -> bool {
match event {
MacroEvent::Define { name, .. } | MacroEvent::Undef { name, .. } => {
guards.iter().any(|guard| guard.may_depend_on_macro(name))
}
MacroEvent::Include { targets, .. } => {
targets.is_empty()
|| targets
.iter()
.any(|target| self.source_may_mutate_guards(target, guards, visited))
}
MacroEvent::Invalidate { .. } => true,
}
}
fn source_may_mutate_guards(
&self,
file: &ProjectFile,
guards: &HashSet<PreprocessorGuard>,
visited: &mut HashSet<ProjectFile>,
) -> bool {
if !visited.insert(file.clone()) {
return false;
}
let cell = self.macro_event_cell(file);
let events = cell.get_or_init(|| Arc::from(self.collect_macro_events(file)));
events
.iter()
.any(|event| self.macro_event_may_mutate_guards(event, guards, visited))
}
pub fn resolve_type(&self, file: &ProjectFile, raw_name: &str) -> Option<CodeUnit> {
let normalized = normalize_reference_name(raw_name)?;
self.type_candidates(file, &normalized)
.into_iter()
.next()
.cloned()
}
pub fn unique_visible_parameter_type_fallback(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
node: Node<'_>,
source: &str,
) -> Option<CodeUnit> {
if node.kind() != "type_identifier" || !is_parameter_type_reference(node) {
return None;
}
let name = node_text(node, source);
let candidates = self
.visible_identifier_candidates(file, name)
.filter(|candidate| candidate.is_class() || declared_type_alias(analyzer, candidate))
.filter(|candidate| {
self.external_type_candidate_visible_in_context(analyzer, file, candidate, node)
})
.collect::<Vec<_>>();
self.unique_canonical_type_candidate(analyzer, file, &candidates)
}
pub fn resolve_type_node_result(
&self,
file: &ProjectFile,
node: Node<'_>,
source: &str,
) -> std::result::Result<Option<CodeUnit>, CppTemplateResolutionError> {
let Some(primary) = self.resolve_type_node_primary(file, node, source) else {
return Ok(None);
};
let Some(arguments) = cpp_template_reference_arguments(node, source) else {
return Ok(Some(primary));
};
self.resolve_template_arguments(file, primary, &arguments)
.map(Some)
}
pub fn resolve_type_node_primary(
&self,
file: &ProjectFile,
node: Node<'_>,
source: &str,
) -> Option<CodeUnit> {
let components = cpp_type_name_components(node, source)?;
if !is_globally_qualified_cpp_name(node)
&& let Some(resolution) =
function_local_type_resolution(&self.cpp_source(), file, node, &components, source)
{
return match resolution {
LexicalTypeResolution::Resolved { unit, .. } => Some(unit),
LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => None,
};
}
self.resolve_type(file, &components.join("::"))
}
pub fn resolve_template_arguments(
&self,
file: &ProjectFile,
primary: CodeUnit,
arguments: &[CppTemplateExpression],
) -> std::result::Result<CodeUnit, CppTemplateResolutionError> {
self.resolve_template_arguments_inner(file, primary, arguments, &mut HashSet::default())
}
fn resolve_template_arguments_inner(
&self,
file: &ProjectFile,
primary: CodeUnit,
arguments: &[CppTemplateExpression],
seen_aliases: &mut HashSet<CodeUnit>,
) -> std::result::Result<CodeUnit, CppTemplateResolutionError> {
if let Some(metadata) = self.cpp_template_metadata.get(&primary)
&& let Some(alias_target) = &metadata.alias_target
{
if !seen_aliases.insert(primary.clone()) {
return Err(CppTemplateResolutionError::AliasCycle { alias: primary });
}
let (_, bindings) = cpp_bind_template_arguments(&metadata.parameters, arguments)
.ok_or(CppTemplateResolutionError::ArgumentBinding)?;
let target_name = alias_target.components.join("::");
let target_primary = if alias_target.global {
unique_logical_type_candidate(self.type_candidates(file, &target_name))
} else {
self.resolve_unique_type_for_declaration(file, &primary, &target_name)
};
let Some(target_primary) = target_primary else {
return Ok(primary);
};
let Some(target_arguments) = &alias_target.arguments else {
return Ok(target_primary);
};
let target_arguments = cpp_substitute_template_arguments(target_arguments, &bindings)
.ok_or(CppTemplateResolutionError::Substitution)?;
return self.resolve_template_arguments_inner(
file,
target_primary,
&target_arguments,
seen_aliases,
);
}
let primary_fq_name = self
.cpp_template_metadata
.get(&primary)
.map(|metadata| metadata.primary_fq_name.clone())
.unwrap_or_else(|| primary.fq_name());
let has_specialization_metadata = self
.cpp_template_families
.get(&primary_fq_name)
.is_some_and(|family| family.iter().any(|unit| self.is_visible(file, unit)));
if !has_specialization_metadata {
return Ok(primary);
}
self.select_template_specialization(file, &primary, arguments)
}
fn select_template_specialization(
&self,
file: &ProjectFile,
resolved: &CodeUnit,
explicit_arguments: &[CppTemplateExpression],
) -> std::result::Result<CodeUnit, CppTemplateResolutionError> {
let primary_fq_name = self
.cpp_template_metadata
.get(resolved)
.map(|metadata| metadata.primary_fq_name.clone())
.unwrap_or_else(|| resolved.fq_name());
let family = self
.cpp_template_families
.get(&primary_fq_name)
.ok_or(CppTemplateResolutionError::PrimarySelection)?;
let primary_candidates = family
.iter()
.filter_map(|unit| {
let metadata = self.cpp_template_metadata.get(unit)?;
(metadata.is_primary() && self.is_visible(file, unit)).then_some((unit, metadata))
})
.collect::<Vec<_>>();
let primary_unit = primary_candidates
.iter()
.find_map(|(unit, _)| (*unit == resolved).then_some(*unit))
.or_else(|| {
primary_candidates
.iter()
.map(|(unit, _)| *unit)
.min_by_key(|unit| {
(
unit.source().to_string(),
unit.signature().unwrap_or_default(),
)
})
})
.ok_or(CppTemplateResolutionError::PrimarySelection)?;
let primary_parameters =
cpp_reconcile_primary_template_parameters(&primary_candidates, primary_unit)
.ok_or(CppTemplateResolutionError::PrimarySelection)?;
let (expanded, _) = cpp_bind_template_arguments(&primary_parameters, explicit_arguments)
.ok_or(CppTemplateResolutionError::ArgumentBinding)?;
let mut applicable = Vec::new();
for unit in family {
let Some(metadata) = self.cpp_template_metadata.get(unit) else {
continue;
};
if metadata.is_primary() || !self.is_visible(file, unit) {
continue;
}
if !cpp_specialization_matches(metadata, &expanded) {
continue;
}
applicable.push((unit, metadata));
}
if applicable.is_empty() {
return Ok(primary_unit.clone());
}
let winners = applicable
.iter()
.filter(|(candidate, candidate_metadata)| {
applicable.iter().all(|(other, other_metadata)| {
same_visible_symbol(candidate, other)
|| cpp_specialization_more_specialized(candidate_metadata, other_metadata)
})
})
.copied()
.collect::<Vec<_>>();
let Some((selected, _)) = winners.first() else {
return Err(CppTemplateResolutionError::AmbiguousSpecialization {
candidates: distinct_visible_symbols(applicable.iter().map(|(unit, _)| *unit)),
});
};
if winners
.iter()
.any(|(unit, _)| !same_visible_symbol(unit, selected))
{
return Err(CppTemplateResolutionError::AmbiguousSpecialization {
candidates: distinct_visible_symbols(winners.iter().map(|(unit, _)| *unit)),
});
}
Ok((*selected).clone())
}
pub fn resolve_type_components_lexically(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
components: &[String],
global: bool,
lexical_scope: &[String],
) -> LexicalTypeResolution {
self.resolve_type_components_lexically_inner(
analyzer,
file,
components,
global,
lexical_scope,
TypeCandidateResolution::Canonical,
)
}
pub fn resolve_type_components_lexically_for_forward(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
components: &[String],
global: bool,
lexical_scope: &[String],
) -> LexicalTypeResolution {
self.resolve_type_components_lexically_inner(
analyzer,
file,
components,
global,
lexical_scope,
TypeCandidateResolution::PreserveAlias,
)
}
pub fn resolve_type_components_lexically_for_target(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
components: &[String],
global: bool,
lexical_scope: &[String],
target: &CodeUnit,
) -> LexicalTypeResolution {
#[cfg(any(test, feature = "test-support"))]
self.target_preserving_type_resolution_count
.fetch_add(1, Ordering::Relaxed);
self.resolve_type_components_lexically_inner(
analyzer,
file,
components,
global,
lexical_scope,
TypeCandidateResolution::PreserveTarget(target),
)
}
pub fn coarse_unqualified_type_reference_may_resolve(
&self,
file: &ProjectFile,
name: &str,
) -> bool {
if name.is_empty() {
return true;
}
self.visible_identifier_candidates(file, name)
.any(|candidate| {
candidate.kind() == CodeUnitType::Class
|| declared_type_alias(&self.cpp_source(), candidate)
})
|| self.visible_parser_alias_name_is_visible(file, name)
}
#[allow(clippy::too_many_arguments)]
pub fn structured_type_reference_may_resolve_to_target(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
components: &[String],
global: bool,
lexical_scope: &[String],
target: &CodeUnit,
) -> bool {
if components.is_empty() {
return true;
}
let Some(terminal) = components.last() else {
return true;
};
let qualified_tiers = lexical_component_tiers(components, global, lexical_scope)
.map(|qualified| qualified.join("::"))
.collect::<Vec<_>>();
let target_name = cpp_name_for(target);
if qualified_tiers
.iter()
.any(|qualified| qualified == &target_name)
{
return true;
}
let mut saw_shape_candidate = false;
for candidate in self.visible_identifier_candidates(file, terminal) {
if candidate.kind() != CodeUnitType::Class && !declared_type_alias(analyzer, candidate)
{
continue;
}
let candidate_name = cpp_name_for(candidate);
let shape_matches = if global || components.len() > 1 {
qualified_tiers
.iter()
.any(|qualified| qualified == &candidate_name)
} else {
true
};
if !shape_matches {
continue;
}
saw_shape_candidate = true;
if same_visible_symbol(candidate, target)
|| self.c_tag_declaration_family_matches_target(
analyzer,
file,
std::slice::from_ref(&candidate),
target,
)
|| self.compatible_primary_template_redeclarations(candidate, target)
|| (declared_type_alias(analyzer, candidate)
&& self.alias_candidate_may_preserve_target(analyzer, file, candidate, target))
{
return true;
}
}
!saw_shape_candidate
}
fn cached_c_tag_kind(
&self,
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> Option<CppCTagKind> {
visibility_memo_value(
&self.c_tag_kind_cache,
&self.reads,
candidate.clone(),
|| indexed_c_tag_kind(analyzer, candidate),
)
}
fn cached_unique_c_tag_complete_definition(
&self,
analyzer: &CppGraphSource<'_>,
target: &CodeUnit,
target_tag: CppCTagKind,
) -> Option<CodeUnit> {
visibility_memo_value(
&self.c_tag_complete_definition_cache,
&self.reads,
target.clone(),
|| {
let complete_definitions = analyzer
.definitions(&target.fq_name())
.filter(|candidate| {
candidate.is_class()
&& !declared_type_alias(analyzer, candidate)
&& is_c_source_file(candidate.source())
&& analyzer.parent_of(candidate).is_none()
&& cpp_class_declaration_strength(analyzer, candidate)
== CppClassDeclarationStrength::Full
&& self.cached_c_tag_kind(analyzer, candidate) == Some(target_tag)
})
.collect::<HashSet<_>>();
(complete_definitions.len() == 1)
.then(|| complete_definitions.into_iter().next())
.flatten()
.filter(|candidate| same_visible_symbol(candidate, target))
},
)
}
pub fn c_tag_declaration_family_matches_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidates: &[&CodeUnit],
target: &CodeUnit,
) -> bool {
if std::env::var_os("BIFROST_CPP_VISIBILITY_STATS").is_some() {
let candidate_evidence = candidates
.iter()
.map(|candidate| {
(
candidate.fq_name(),
candidate.source().rel_path().to_path_buf(),
cpp_class_declaration_strength(analyzer, candidate),
indexed_c_tag_kind(analyzer, candidate),
self.is_physically_visible(visible_from, candidate),
)
})
.collect::<Vec<_>>();
eprintln!(
"BIFROST_CPP_C_TAG_FAMILY_STATS visible_from={} target=({}, {}, {:?}, {:?}) candidates={candidate_evidence:?}",
visible_from.rel_path().display(),
target.fq_name(),
target.source().rel_path().display(),
cpp_class_declaration_strength(analyzer, target),
indexed_c_tag_kind(analyzer, target),
);
}
if candidates.is_empty()
|| !target.is_class()
|| declared_type_alias(analyzer, target)
|| !is_c_source_file(target.source())
|| analyzer.parent_of(target).is_some()
|| cpp_class_declaration_strength(analyzer, target) != CppClassDeclarationStrength::Full
{
return false;
}
let Some(target_tag) = self.cached_c_tag_kind(analyzer, target) else {
return false;
};
if self
.cached_unique_c_tag_complete_definition(analyzer, target, target_tag)
.is_none()
{
return false;
}
let mut saw_visible_forward = false;
for candidate in candidates.iter().copied() {
if candidate == target {
continue;
}
if !candidate.is_class()
|| declared_type_alias(analyzer, candidate)
|| candidate.fq_name() != target.fq_name()
|| analyzer.parent_of(candidate).is_some()
|| cpp_class_declaration_strength(analyzer, candidate)
!= CppClassDeclarationStrength::Forward
|| self.cached_c_tag_kind(analyzer, candidate) != Some(target_tag)
|| !self.is_physically_visible(visible_from, candidate)
{
return false;
}
saw_visible_forward = true;
}
saw_visible_forward
}
fn unique_c_tag_declaration_family(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidates: &[&CodeUnit],
) -> Option<CodeUnit> {
let first = candidates.first()?;
let target_fq_name = first.fq_name();
let target_tag = self.cached_c_tag_kind(analyzer, first)?;
let mut full = None;
let mut saw_forward = false;
for candidate in candidates.iter().copied() {
if !candidate.is_class()
|| declared_type_alias(analyzer, candidate)
|| candidate.fq_name() != target_fq_name
|| analyzer.parent_of(candidate).is_some()
|| self.cached_c_tag_kind(analyzer, candidate) != Some(target_tag)
{
return None;
}
match cpp_class_declaration_strength(analyzer, candidate) {
CppClassDeclarationStrength::Full
if is_c_source_file(candidate.source())
&& full.replace(candidate.clone()).is_none() => {}
CppClassDeclarationStrength::Forward
if self.is_physically_visible(visible_from, candidate) =>
{
saw_forward = true
}
_ => return None,
}
}
if saw_forward { full } else { None }
}
pub fn target_preserving_reference_namespace(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
identifier: &str,
target: &CodeUnit,
) -> Option<Vec<String>> {
let mut namespace = None;
for candidate in self.visible_identifier_candidates(file, identifier) {
if candidate.kind() != CodeUnitType::Class && !declared_type_alias(analyzer, candidate)
{
continue;
}
if !(same_visible_symbol(candidate, target)
|| self.compatible_primary_template_redeclarations(candidate, target)
|| declared_type_alias(analyzer, candidate)
&& self.structured_alias_primary_preserves_target(
analyzer, file, candidate, target,
))
{
continue;
}
if namespace
.as_ref()
.is_some_and(|existing| existing != candidate.package_name())
{
return None;
}
namespace = Some(candidate.package_name().to_string());
}
let namespace = namespace?;
Some(
brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
&namespace,
),
)
}
pub fn resolve_imported_type_candidate(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
target: &CodeUnit,
target_components: &[String],
direct_target: Option<&CodeUnit>,
preserve_alias: bool,
) -> LexicalTypeResolution {
let candidates = [target];
let resolution = if preserve_alias {
TypeCandidateResolution::PreserveAlias
} else {
direct_target.map_or(
TypeCandidateResolution::Canonical,
TypeCandidateResolution::PreserveTarget,
)
};
match self.resolve_type_candidates(analyzer, file, &candidates, resolution) {
Ok(unit) => LexicalTypeResolution::Resolved {
unit,
components: target_components.to_vec(),
candidates: vec![target.clone()],
},
Err(failure) => failure.lexical_resolution(),
}
}
fn resolve_type_components_lexically_inner(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
components: &[String],
global: bool,
lexical_scope: &[String],
resolution: TypeCandidateResolution<'_>,
) -> LexicalTypeResolution {
if components.is_empty() {
return LexicalTypeResolution::Missing;
}
let mut injected = self.resolve_injected_class_name(
analyzer,
file,
components,
global,
lexical_scope,
resolution,
);
for qualified in lexical_component_tiers(components, global, lexical_scope) {
let prefix_len = qualified.len().saturating_sub(components.len());
if injected
.as_ref()
.is_some_and(|(owner_len, _)| prefix_len <= *owner_len)
{
return injected
.take()
.expect("injected class resolution was just present")
.1;
}
let qualified_name = qualified.join("::");
let candidates = self
.type_candidates(file, &qualified_name)
.into_iter()
.filter(|candidate| canonical_cpp_name_matches(candidate, &qualified_name))
.collect::<Vec<_>>();
if candidates.is_empty() {
if !global && components.len() == 1 {
match self.resolve_inherited_type_for_lexical_scope(
analyzer,
file,
&qualified[..prefix_len],
&components[0],
resolution,
) {
LexicalTypeResolution::Missing => {}
inherited => return inherited,
}
}
continue;
}
let candidates =
self.candidates_for_type_resolution(analyzer, file, &candidates, resolution);
let unit = match self.resolve_type_candidates(analyzer, file, &candidates, resolution) {
Ok(unit) => unit,
Err(failure) => return failure.lexical_resolution(),
};
return LexicalTypeResolution::Resolved {
unit,
components: qualified,
candidates: candidates.into_iter().cloned().collect(),
};
}
LexicalTypeResolution::Missing
}
fn resolve_injected_class_name(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
components: &[String],
global: bool,
lexical_scope: &[String],
resolution: TypeCandidateResolution<'_>,
) -> Option<(usize, LexicalTypeResolution)> {
if global
|| components.len() != 1
|| file.rel_path().extension().is_some_and(|ext| ext == "c")
|| matches!(resolution, TypeCandidateResolution::PreserveTarget(target) if !target.is_class())
{
return None;
}
let name = components.first()?;
let mut matches: Vec<&CodeUnit> = Vec::new();
let mut owner_len = 0;
for candidate in self.visible_identifier_candidates(file, name) {
if !candidate.is_class()
|| declared_type_alias(analyzer, candidate)
|| candidate.identifier() != name
{
continue;
}
let candidate_scope = canonical_cpp_scope_components(candidate);
if candidate_scope.len() > lexical_scope.len()
|| !lexical_scope.starts_with(&candidate_scope)
|| candidate_scope.last().is_none_or(|last| last != name)
{
continue;
}
if candidate_scope.len() > owner_len {
owner_len = candidate_scope.len();
matches.clear();
}
if candidate_scope.len() == owner_len
&& !matches
.iter()
.any(|existing| same_logical_symbol(existing, candidate))
{
matches.push(candidate);
}
}
if matches.is_empty() {
return None;
}
let owner_components = lexical_scope[..owner_len].to_vec();
let matches = self.candidates_for_type_resolution(analyzer, file, &matches, resolution);
let resolution = match self.resolve_type_candidates(analyzer, file, &matches, resolution) {
Ok(unit) => LexicalTypeResolution::Resolved {
unit,
components: owner_components,
candidates: matches.into_iter().cloned().collect(),
},
Err(failure) => failure.lexical_resolution(),
};
Some((owner_len, resolution))
}
fn resolve_inherited_type_for_lexical_scope(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
lexical_scope: &[String],
name: &str,
resolution: TypeCandidateResolution<'_>,
) -> LexicalTypeResolution {
let Some(hierarchy) = analyzer.type_hierarchy_provider() else {
return LexicalTypeResolution::Missing;
};
let lexical_owner_name = lexical_scope.join("::");
if lexical_owner_name.is_empty() {
return LexicalTypeResolution::Missing;
}
let owner_candidates = self
.type_candidates(file, &lexical_owner_name)
.into_iter()
.filter(|candidate| {
canonical_cpp_name_matches(candidate, &lexical_owner_name)
&& !declared_type_alias(analyzer, candidate)
})
.collect::<Vec<_>>();
if owner_candidates.is_empty() {
return LexicalTypeResolution::Missing;
}
let physical_owner_candidates = owner_candidates
.iter()
.copied()
.filter(|candidate| candidate.source() == file)
.collect::<Vec<_>>();
let lexical_owner_candidates = if physical_owner_candidates.is_empty() {
owner_candidates
} else {
physical_owner_candidates
};
let Some(lexical_owner) = unique_logical_type_candidate(lexical_owner_candidates) else {
return LexicalTypeResolution::Ambiguous;
};
let mut frontier = hierarchy
.get_direct_ancestors(&lexical_owner)
.definite()
.to_vec();
let mut visited_owners = HashSet::default();
while !frontier.is_empty() {
let mut level_matches: Vec<(CodeUnit, Vec<CodeUnit>)> = Vec::new();
let mut next_frontier = Vec::new();
for owner in frontier {
if !visited_owners.insert(owner.fq_name()) {
continue;
}
let qualified_name = format!("{}::{name}", cpp_name_for(&owner));
let candidates = self
.type_candidates(file, &qualified_name)
.into_iter()
.filter(|candidate| canonical_cpp_name_matches(candidate, &qualified_name))
.collect::<Vec<_>>();
if candidates.is_empty() {
for ancestor in hierarchy.get_direct_ancestors(&owner).definite().to_vec() {
if !next_frontier
.iter()
.any(|existing: &CodeUnit| existing.fq_name() == ancestor.fq_name())
{
next_frontier.push(ancestor);
}
}
continue;
}
let candidates =
self.candidates_for_type_resolution(analyzer, file, &candidates, resolution);
let unit =
match self.resolve_type_candidates(analyzer, file, &candidates, resolution) {
Ok(unit) => unit,
Err(failure) => return failure.lexical_resolution(),
};
level_matches.push((unit, candidates.into_iter().cloned().collect::<Vec<_>>()));
}
if let Some((unit, candidates)) = level_matches.first().cloned() {
let Some(first_declaration) = candidates.first() else {
return LexicalTypeResolution::Ambiguous;
};
if !level_matches.iter().all(|(_, declarations)| {
declarations
.iter()
.all(|declaration| same_logical_symbol(first_declaration, declaration))
}) {
return LexicalTypeResolution::Ambiguous;
}
let mut components = lexical_scope.to_vec();
components.push(name.to_string());
return LexicalTypeResolution::Resolved {
unit,
components,
candidates,
};
}
frontier = next_frontier;
}
LexicalTypeResolution::Missing
}
pub fn inherited_injected_class_owner(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
enclosing_owner: &CodeUnit,
injected_name: &str,
) -> Option<CodeUnit> {
let hierarchy = analyzer.type_hierarchy_provider()?;
let mut frontier = hierarchy
.get_direct_ancestors(enclosing_owner)
.definite()
.to_vec();
let mut propagated_counts: HashMap<CodeUnit, u8> = HashMap::default();
while !frontier.is_empty() {
let mut level_matches = Vec::new();
let mut next_frontier = Vec::new();
for raw_owner in frontier {
let Some(owner) = self.canonical_visible_full_type_unit(analyzer, file, &raw_owner)
else {
if raw_owner.identifier() == injected_name {
return None;
}
continue;
};
let propagated = propagated_counts.entry(owner.clone()).or_default();
if *propagated == 2 {
continue;
}
*propagated += 1;
if owner.identifier() == injected_name {
level_matches.push(owner.clone());
}
next_frontier.extend(hierarchy.get_direct_ancestors(&owner).definite().to_vec());
}
match level_matches.as_slice() {
[owner] => return Some(owner.clone()),
[_, ..] => return None,
[] => {}
}
frontier = next_frontier;
}
None
}
fn resolve_type_candidates(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidates: &[&CodeUnit],
resolution: TypeCandidateResolution<'_>,
) -> Result<CodeUnit, TypeCandidateFailure> {
if !matches!(resolution, TypeCandidateResolution::PreserveTarget(_))
&& let Some(unit) = self.unique_c_tag_declaration_family(analyzer, file, candidates)
{
return Ok(unit);
}
match resolution {
TypeCandidateResolution::Canonical => {
self.canonical_type_candidate_resolution(analyzer, file, candidates)
}
TypeCandidateResolution::PreserveAlias => {
let same_fqn_alias_family = candidates.len() > 1
&& candidates.iter().all(|candidate| {
declared_type_alias(analyzer, candidate)
&& same_logical_symbol(candidates[0], candidate)
})
&& candidates
.iter()
.any(|candidate| candidate.source() != candidates[0].source());
if same_fqn_alias_family {
let physically_visible = candidates
.iter()
.copied()
.filter(|candidate| self.is_physically_visible(file, candidate))
.collect::<Vec<_>>();
if let Some(alias) = select_alias_spelling_with_agreed_targets(
&physically_visible,
|candidate| self.structured_alias_target(analyzer, candidate),
) {
return Ok(alias.clone());
}
}
unique_type_candidate_preserving_alias(analyzer, file, candidates)
.ok_or(TypeCandidateFailure::Ambiguous)
}
TypeCandidateResolution::PreserveTarget(target) => self
.unique_type_candidate_preserving_target(analyzer, file, candidates, target)
.ok_or(TypeCandidateFailure::Ambiguous),
}
}
fn candidates_for_type_resolution<'b>(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidates: &[&'b CodeUnit],
resolution: TypeCandidateResolution<'_>,
) -> Vec<&'b CodeUnit> {
if matches!(resolution, TypeCandidateResolution::PreserveAlias) && candidates.len() > 1 {
let compile_proven = self.compile_proven_type_candidates(analyzer, file, candidates);
if compile_proven.len() == 1 {
return compile_proven;
}
}
candidates.to_vec()
}
fn compile_proven_type_candidates<'b>(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidates: &[&'b CodeUnit],
) -> Vec<&'b CodeUnit> {
let proven = self.compile_proven_guards(file);
if proven.is_empty() {
return Vec::new();
}
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return Vec::new();
};
candidates
.iter()
.copied()
.filter(|candidate| {
let declaration_guards =
declaration_guard_requirements(analyzer, self.cpp, candidate);
if declaration_guards.is_empty() {
return false;
}
if candidate.source() == file {
return declaration_guards.iter().any(|(_, required)| {
guard_requirements_hold_at_reference(required, Some(proven.as_ref()))
});
}
declaration_guards.iter().any(|(_, required)| {
self.foreign_declaration_reachable_from_compile_proven_guards(
file,
prepared.as_ref(),
candidate.source(),
required,
usize::MAX,
)
})
})
.collect()
}
pub fn resolve_callable_value_components_lexically(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
owner_components: &[String],
member_name: &str,
global: bool,
lexical_scope: &[String],
) -> LexicalCallableValueResolution {
if owner_components.is_empty() || member_name.is_empty() {
return LexicalCallableValueResolution::Missing;
}
for qualified_owner in lexical_component_tiers(owner_components, global, lexical_scope) {
let owner_name = qualified_owner.join("::");
let type_candidates = self
.type_candidates(file, &owner_name)
.into_iter()
.filter(|candidate| canonical_cpp_name_matches(candidate, &owner_name))
.collect::<Vec<_>>();
let resolved_type = if type_candidates.is_empty() {
None
} else {
let Some(unit) =
self.unique_canonical_type_candidate(analyzer, file, &type_candidates)
else {
return LexicalCallableValueResolution::Ambiguous;
};
Some(unit)
};
let mut qualified_callable = qualified_owner;
qualified_callable.push(member_name.to_string());
let callable_name = qualified_callable.join("::");
let free_function = self
.named_candidates_for_normalized(file, &callable_name, TargetKind::FreeFunction)
.into_iter()
.find(|candidate| {
canonical_cpp_name_matches(candidate, &callable_name)
&& type_owner_of(analyzer, candidate).is_none()
})
.cloned();
match (resolved_type, free_function) {
(Some(_), Some(_)) => return LexicalCallableValueResolution::Ambiguous,
(Some(owner), None) => return LexicalCallableValueResolution::Type(owner),
(None, Some(function)) => {
return LexicalCallableValueResolution::FreeFunction(function);
}
(None, None) => {}
}
}
LexicalCallableValueResolution::Missing
}
fn resolve_type_for_declaration(
&self,
visible_from: &ProjectFile,
declaration: &CodeUnit,
raw_name: &str,
) -> Option<CodeUnit> {
let normalized = normalize_reference_name(raw_name)?;
if !normalized.contains("::")
&& let Some(namespace) = cpp_namespace_for(declaration)
{
for prefix in namespace_prefixes(&namespace) {
let qualified = format!("{prefix}::{normalized}");
if let Some(unit) = self
.type_candidates(visible_from, &qualified)
.into_iter()
.next()
{
return Some(unit.clone());
}
}
}
self.resolve_type(visible_from, raw_name)
}
fn resolve_unique_canonical_type_for_declaration(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
declaration: &CodeUnit,
raw_name: &str,
) -> Option<CodeUnit> {
let current = self.resolve_defining_type_for_declaration(
analyzer,
visible_from,
declaration,
raw_name,
)?;
self.canonical_type_resolution(analyzer, visible_from, ¤t)
.ok()
}
pub fn canonical_type_unit(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
unit: &CodeUnit,
) -> Option<CodeUnit> {
self.canonical_type_resolution(analyzer, visible_from, unit)
.ok()
}
pub fn canonical_type_unit_in_context(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
reference: Node<'_>,
unit: &CodeUnit,
) -> Option<CodeUnit> {
if !self.external_type_candidate_visible_in_context(analyzer, visible_from, unit, reference)
{
return None;
}
self.canonical_type_resolution(analyzer, visible_from, unit)
.ok()
}
pub fn guarded_alias_selection_at_reference(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
reference: Node<'_>,
candidates: &[CodeUnit],
) -> Option<GuardedAliasSelection> {
const MAX_GUARDED_ALIAS_STEPS: usize = 64;
let mut conditional = false;
let mut complete = true;
let mut selected = Vec::new();
let mut terminals: Vec<(CodeUnit, bool)> = Vec::new();
let mut visited = HashSet::default();
let mut pending = candidates
.iter()
.map(|candidate| (candidate.clone(), true, 0, true))
.collect::<Vec<_>>();
let mut steps = 0;
while let Some((unit, proven, depth, top_level)) = pending.pop() {
if !visited.insert((unit.clone(), proven)) {
continue;
}
steps += 1;
if steps > MAX_GUARDED_ALIAS_STEPS || depth > MAX_COMPARABLE_ALIAS_HOPS {
return None;
}
let arms = match self.conditional_alias_arms(analyzer, file, &unit) {
Some(arms) => {
conditional = true;
let evidence = arms
.into_iter()
.map(|arm| {
let evidence = self.alias_arm_evidence(analyzer, file, &arm, reference);
(arm, evidence)
})
.collect::<Vec<_>>();
let implied = evidence
.iter()
.any(|(_, evidence)| *evidence == AliasArmEvidence::Implied);
for (arm, _) in &evidence {
visited.insert((arm.clone(), proven));
}
evidence
.into_iter()
.filter_map(|(arm, evidence)| match evidence {
AliasArmEvidence::Implied => Some((arm, proven)),
AliasArmEvidence::Compatible if !implied => Some((arm, false)),
AliasArmEvidence::Compatible | AliasArmEvidence::Contradicted => None,
})
.collect::<Vec<_>>()
}
None => vec![(unit, proven)],
};
for (arm, proven) in arms {
if top_level {
selected.push(arm.clone());
}
match self.structured_alias_target(analyzer, &arm) {
None | Some(StructuredAliasTarget::Builtin) if arm.is_class() => {
terminals.push((arm, proven));
}
None | Some(StructuredAliasTarget::Builtin) => complete = false,
Some(target) => {
match self.structured_alias_target_resolution(file, &arm, &target) {
Ok(next) => pending.push((next, proven, depth + 1, false)),
Err(_) => complete = false,
}
}
}
}
}
if !conditional {
return None;
}
let mut targets: Vec<CodeUnit> = Vec::new();
for (terminal, _) in &terminals {
if !targets
.iter()
.any(|existing| same_visible_symbol(existing, terminal))
{
targets.push(terminal.clone());
}
}
if complete
&& let [unit] = targets.as_slice()
&& terminals.iter().all(|(_, proven)| *proven)
{
return Some(GuardedAliasSelection::Proven {
unit: unit.clone(),
candidates: selected,
});
}
Some(GuardedAliasSelection::Alternatives(targets))
}
fn conditional_alias_arms(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
unit: &CodeUnit,
) -> Option<Vec<CodeUnit>> {
if !declared_type_alias(analyzer, unit) || self.cpp_template_metadata.contains_key(unit) {
return None;
}
let mut arms = Vec::new();
for candidate in self
.visible_identifier_candidates(file, unit.identifier())
.filter(|candidate| {
candidate.kind() == unit.kind()
&& candidate.fq_name() == unit.fq_name()
&& declared_type_alias(analyzer, candidate)
})
{
if self.cpp_template_metadata.contains_key(candidate) {
return None;
}
if !arms.contains(candidate) {
arms.push(candidate.clone());
}
}
let refs = arms.iter().collect::<Vec<_>>();
(arms.len() > 1
&& select_alias_spelling_with_agreed_targets(&refs, |candidate| {
self.structured_alias_target(analyzer, candidate)
})
.is_none())
.then_some(arms)
}
fn alias_arm_evidence(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
arm: &CodeUnit,
reference: Node<'_>,
) -> AliasArmEvidence {
let Some(prepared) = self.cpp.prepared_syntax(self.token, file) else {
return AliasArmEvidence::Compatible;
};
let effective = self.effective_reference_guards(
file,
preprocessor_guard_environment(reference, prepared.source()).as_ref(),
);
let requirements = declaration_guard_requirements(analyzer, self.cpp, arm);
if requirements
.iter()
.any(|(_, required)| guard_requirements_hold_at_reference(required, effective.as_ref()))
&& self.external_type_candidate_visible_in_context(analyzer, file, arm, reference)
{
AliasArmEvidence::Implied
} else if requirements.is_empty()
|| requirements
.iter()
.any(|(_, required)| guards_compatible_at_reference(required, effective.as_ref()))
{
AliasArmEvidence::Compatible
} else {
AliasArmEvidence::Contradicted
}
}
fn canonical_type_resolution(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
unit: &CodeUnit,
) -> Result<CodeUnit, TypeCandidateFailure> {
let mut current = unit.clone();
let mut seen_aliases = HashSet::default();
loop {
let Some(target) = self.structured_alias_target(analyzer, ¤t) else {
return current
.is_class()
.then_some(current)
.ok_or(TypeCandidateFailure::Unresolvable);
};
if matches!(target, StructuredAliasTarget::Builtin) {
return current
.is_class()
.then_some(current)
.ok_or(TypeCandidateFailure::Unresolvable);
}
if !seen_aliases.insert(current.clone()) {
return Err(TypeCandidateFailure::Unresolvable);
}
current = self.structured_alias_target_resolution(visible_from, ¤t, &target)?;
}
}
pub fn canonical_visible_full_type_unit(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
unit: &CodeUnit,
) -> Option<CodeUnit> {
let canonical = self.canonical_type_unit(analyzer, visible_from, unit)?;
if cpp_class_declaration_strength(analyzer, &canonical)
!= CppClassDeclarationStrength::Forward
{
return Some(canonical);
}
let mut full = Vec::new();
for candidate in self
.visible_identifier_candidates(visible_from, canonical.identifier())
.filter(|candidate| {
candidate.is_class()
&& candidate.fq_name() == canonical.fq_name()
&& cpp_class_declaration_strength(analyzer, candidate)
== CppClassDeclarationStrength::Full
})
{
if !full.iter().any(|existing| same_symbol(existing, candidate)) {
full.push(candidate.clone());
}
}
match full.len() {
0 => Some(canonical),
1 => full.pop(),
_ => None,
}
}
fn resolve_structured_alias_target(
&self,
visible_from: &ProjectFile,
declaration: &CodeUnit,
target: &StructuredAliasTarget,
) -> Option<CodeUnit> {
self.structured_alias_target_resolution(visible_from, declaration, target)
.ok()
}
fn structured_alias_target_resolution(
&self,
visible_from: &ProjectFile,
declaration: &CodeUnit,
target: &StructuredAliasTarget,
) -> Result<CodeUnit, TypeCandidateFailure> {
let primary =
self.structured_alias_primary_resolution(visible_from, declaration, target)?;
let StructuredAliasTarget::Named { arguments, .. } = target else {
return Err(TypeCandidateFailure::Unresolvable);
};
match arguments {
Some(arguments) => self
.resolve_template_arguments(visible_from, primary, arguments)
.map_err(|error| match error {
CppTemplateResolutionError::AmbiguousSpecialization { .. } => {
TypeCandidateFailure::Ambiguous
}
_ => TypeCandidateFailure::Unresolvable,
}),
None => Ok(primary),
}
}
fn resolve_structured_alias_primary(
&self,
visible_from: &ProjectFile,
declaration: &CodeUnit,
target: &StructuredAliasTarget,
) -> Option<CodeUnit> {
self.structured_alias_primary_resolution(visible_from, declaration, target)
.ok()
}
fn structured_alias_primary_resolution(
&self,
visible_from: &ProjectFile,
declaration: &CodeUnit,
target: &StructuredAliasTarget,
) -> Result<CodeUnit, TypeCandidateFailure> {
let StructuredAliasTarget::Named {
components, global, ..
} = target
else {
return Err(TypeCandidateFailure::Unresolvable);
};
let qualified = components.join("::");
let candidates = if *global {
let mut candidates = self.type_candidates(visible_from, &qualified);
candidates.retain(|candidate| canonical_cpp_scope_components(candidate) == *components);
candidates
} else {
self.type_candidates_for_declaration(visible_from, declaration, &qualified)
};
logical_type_candidate(candidates)
}
pub fn structured_alias_primary_preserves_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidate: &CodeUnit,
target: &CodeUnit,
) -> bool {
let mut current = candidate.clone();
let mut seen = HashSet::default();
let mut matched_target = false;
loop {
if same_visible_symbol(¤t, target)
|| self.compatible_primary_template_redeclarations(¤t, target)
{
matched_target = true;
}
if !seen.insert(current.clone()) {
return false;
}
let Some(alias_target) = self.structured_alias_target(analyzer, ¤t) else {
return matched_target;
};
if matches!(alias_target, StructuredAliasTarget::Builtin) {
return matched_target;
};
let Some(primary) =
self.resolve_structured_alias_primary(visible_from, ¤t, &alias_target)
else {
return matched_target;
};
current = primary;
}
}
fn lexical_type_candidates_match_target(
&self,
analyzer: &CppGraphSource<'_>,
unit: &CodeUnit,
candidates: &[CodeUnit],
target: &CodeUnit,
) -> bool {
same_visible_symbol(unit, target)
|| self.same_template_member_identity(analyzer, unit, target)
|| candidates.iter().any(|candidate| {
same_visible_symbol(candidate, target)
|| self.same_template_member_identity(analyzer, candidate, target)
})
}
pub fn structured_class_alias_resolves_to_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
alias: &CodeUnit,
target: &CodeUnit,
) -> bool {
let Some(owner) = type_owner_of(analyzer, alias).filter(CodeUnit::is_class) else {
return false;
};
let Some(alias_target) = self.structured_alias_target(analyzer, alias) else {
return false;
};
let StructuredAliasTarget::Named {
components, global, ..
} = &alias_target
else {
return false;
};
let lexical_scope = canonical_cpp_scope_components(&owner);
match self.resolve_type_components_lexically_for_target(
analyzer,
visible_from,
components,
*global,
&lexical_scope,
target,
) {
LexicalTypeResolution::Resolved {
unit, candidates, ..
} => self.lexical_type_candidates_match_target(analyzer, &unit, &candidates, target),
LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => {
self.structured_alias_primary_preserves_target(
analyzer,
visible_from,
alias,
target,
) || self.flattened_macro_namespace_alias_target_matches(
analyzer,
visible_from,
alias,
&alias_target,
target,
)
}
}
}
pub fn structured_class_alias_path_preserves_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
alias: &CodeUnit,
target: &CodeUnit,
) -> bool {
let Some(owner) = type_owner_of(analyzer, alias).filter(CodeUnit::is_class) else {
return false;
};
let Some(StructuredAliasTarget::Named {
components, global, ..
}) = self.structured_alias_target(analyzer, alias)
else {
return false;
};
let lexical_scope = canonical_cpp_scope_components(&owner);
(1..components.len()).rev().any(|component_count| {
matches!(
self.resolve_type_components_lexically_for_target(
analyzer,
visible_from,
&components[..component_count],
global,
&lexical_scope,
target,
),
LexicalTypeResolution::Resolved {
ref unit,
ref candidates,
..
} if self.lexical_type_candidates_match_target(
analyzer,
unit,
candidates,
target,
)
)
})
}
fn flattened_macro_namespace_alias_target_matches(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
alias: &CodeUnit,
alias_target: &StructuredAliasTarget,
target: &CodeUnit,
) -> bool {
let StructuredAliasTarget::Named {
components,
global: false,
arguments: None,
} = alias_target
else {
return false;
};
let Some((target_name, namespace_components)) = components.split_last() else {
return false;
};
if namespace_components.is_empty()
|| target_name != target.identifier()
|| alias.source() != target.source()
|| alias.source() != visible_from
|| !target.is_class()
|| declared_type_alias(analyzer, target)
{
return false;
}
if self
.resolve_structured_alias_target(visible_from, alias, alias_target)
.is_some()
{
return false;
}
let Some(alias_start) = analyzer
.declaration_source_occurrences(alias)
.into_iter()
.flatten()
.map(|occurrence| occurrence.range.start_byte)
.min()
else {
return false;
};
let has_matching_declaration = analyzer
.declaration_source_occurrences(target)
.into_iter()
.flatten()
.any(|occurrence| {
occurrence.range.end_byte <= alias_start
&& occurrence.fact.flattened_macro_namespace.as_deref()
== Some(namespace_components)
});
if !has_matching_declaration {
return false;
}
let alias_guards = declaration_guard_requirements(analyzer, self.cpp, alias);
let target_guards = declaration_guard_requirements(analyzer, self.cpp, target);
guard_requirement_sets_match(&alias_guards, &target_guards)
}
pub fn template_alias_arguments_preserve_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
alias: &CodeUnit,
arguments: &[CppTemplateExpression],
target: &CodeUnit,
) -> bool {
let Some(metadata) = self.cpp_template_metadata.get(alias) else {
return false;
};
if metadata.alias_target.is_none()
|| cpp_bind_template_arguments(&metadata.parameters, arguments).is_none()
{
return false;
}
self.structured_alias_primary_preserves_target(analyzer, visible_from, alias, target)
}
pub fn is_primary_template(&self, unit: &CodeUnit) -> bool {
self.cpp_template_metadata
.get(unit)
.is_some_and(CppTemplateMetadata::is_primary)
}
pub fn is_template_specialization(&self, unit: &CodeUnit) -> bool {
self.cpp_template_metadata
.get(unit)
.is_some_and(CppTemplateMetadata::is_specialization)
}
pub fn same_template_owner_identity(&self, left: &CodeUnit, right: &CodeUnit) -> bool {
same_visible_symbol(left, right)
|| self.compatible_primary_template_redeclarations(left, right)
}
pub fn same_template_member_identity(
&self,
analyzer: &CppGraphSource<'_>,
left: &CodeUnit,
right: &CodeUnit,
) -> bool {
if same_visible_symbol(left, right) {
return true;
}
if left.kind() != right.kind()
|| left.identifier() != right.identifier()
|| left.signature() != right.signature()
{
return false;
}
let (Some(left_owner), Some(right_owner)) =
(analyzer.parent_of(left), analyzer.parent_of(right))
else {
return false;
};
left_owner.is_class()
&& right_owner.is_class()
&& self.same_template_owner_identity(&left_owner, &right_owner)
}
fn unique_canonical_type_candidate(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidates: &[&CodeUnit],
) -> Option<CodeUnit> {
self.canonical_type_candidate_resolution(analyzer, visible_from, candidates)
.ok()
}
fn canonical_type_candidate_resolution(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidates: &[&CodeUnit],
) -> Result<CodeUnit, TypeCandidateFailure> {
let mut canonical = Vec::new();
for candidate in candidates {
let resolved = self.canonical_type_resolution(analyzer, visible_from, candidate)?;
if canonical
.iter()
.any(|existing| same_visible_symbol(existing, &resolved))
{
continue;
}
if let Some(existing) = canonical.iter_mut().find(|existing| {
self.compatible_primary_template_redeclarations(existing, &resolved)
}) {
if matches!(
(
cpp_class_declaration_strength(analyzer, existing),
cpp_class_declaration_strength(analyzer, &resolved),
),
(
CppClassDeclarationStrength::Forward | CppClassDeclarationStrength::Unknown,
CppClassDeclarationStrength::Full,
) | (
CppClassDeclarationStrength::Unknown,
CppClassDeclarationStrength::Forward,
)
) {
*existing = resolved;
}
continue;
}
canonical.push(resolved);
if canonical.len() > 1 {
return Err(TypeCandidateFailure::Ambiguous);
}
}
canonical.pop().ok_or(TypeCandidateFailure::Unresolvable)
}
pub fn unique_type_candidate_preserving_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidates: &[&CodeUnit],
target: &CodeUnit,
) -> Option<CodeUnit> {
if self.c_tag_declaration_family_matches_target(analyzer, visible_from, candidates, target)
|| self.alternate_same_fqn_type_declarations(analyzer, candidates, target)
{
return Some(target.clone());
}
let mut resolved_candidates = Vec::new();
for candidate in candidates {
let Some(resolved) =
self.type_candidate_preserving_target(analyzer, visible_from, candidate, target)
else {
continue;
};
if resolved_candidates
.iter()
.any(|existing| same_visible_symbol(existing, &resolved))
{
continue;
}
resolved_candidates.push(resolved);
}
match resolved_candidates.as_slice() {
[] => None,
[single] => Some(single.clone()),
_ => self
.same_fqn_type_spelling_for_target(analyzer, visible_from, candidates, target)
.map(|_| target.clone()),
}
}
pub fn same_fqn_type_spelling_for_target<'b>(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidates: &[&'b CodeUnit],
target: &CodeUnit,
) -> Option<&'b CodeUnit> {
let [first, rest @ ..] = candidates else {
return None;
};
if rest.is_empty()
|| !rest.iter().all(|candidate| {
candidate.kind() == first.kind()
&& candidate.fq_name() == first.fq_name()
&& candidate.source() == first.source()
})
{
return None;
}
candidates
.iter()
.copied()
.find(|candidate| same_symbol(candidate, target))
.or_else(|| {
candidates.iter().copied().find(|candidate| {
self.type_candidate_preserving_target(analyzer, visible_from, candidate, target)
.is_some_and(|resolved| same_visible_symbol(&resolved, target))
})
})
}
pub fn alternate_same_fqn_type_declarations(
&self,
analyzer: &CppGraphSource<'_>,
candidates: &[&CodeUnit],
target: &CodeUnit,
) -> bool {
let Some(first) = candidates.first() else {
return false;
};
let same_api = first.kind() == target.kind()
&& first.fq_name() == target.fq_name()
&& first.source() == target.source()
&& candidates.iter().all(|candidate| {
candidate.kind() == target.kind()
&& candidate.fq_name() == target.fq_name()
&& candidate.source() == target.source()
})
&& candidates
.iter()
.any(|candidate| same_symbol(candidate, target))
&& candidates
.iter()
.any(|candidate| !same_logical_symbol(candidate, target));
if !same_api {
return false;
}
let requirements = candidates
.iter()
.map(|candidate| declaration_guard_requirements(analyzer, self.cpp, candidate))
.collect::<Vec<_>>();
requirements.len() > 1
&& requirements
.iter()
.all(|requirement| !requirement.is_empty())
&& requirements.iter().enumerate().all(|(index, left)| {
requirements[index + 1..].iter().all(|right| {
left.iter().all(|(_, left_guards)| {
right.iter().all(|(_, right_guards)| {
merge_preprocessor_guards(left_guards, right_guards).is_none()
})
})
})
})
}
fn preprocessor_guard_terms_cover_all_paths(terms: &[HashSet<PreprocessorGuard>]) -> bool {
let mut pending = vec![terms.to_vec()];
while let Some(branch_terms) = pending.pop() {
let mut normalized = Vec::new();
let mut covers_branch = false;
for term in branch_terms {
if term.iter().any(|guard| term.contains(&guard.negated())) {
continue;
}
if term.is_empty() {
covers_branch = true;
break;
}
if !normalized.iter().any(|existing| existing == &term) {
normalized.push(term);
}
}
if covers_branch {
continue;
}
let Some(split_guard) = normalized
.iter()
.flat_map(|term| term.iter())
.next()
.cloned()
else {
return false;
};
let negated_guard = split_guard.negated();
let mut when_defined = Vec::new();
let mut when_undefined = Vec::new();
for term in normalized {
if term.contains(&negated_guard) {
} else if term.contains(&split_guard) {
let mut reduced = term.clone();
reduced.remove(&split_guard);
when_defined.push(reduced);
} else {
when_defined.push(term.clone());
}
if term.contains(&split_guard) {
} else if term.contains(&negated_guard) {
let mut reduced = term;
reduced.remove(&negated_guard);
when_undefined.push(reduced);
} else {
when_undefined.push(term);
}
}
pending.push(when_defined);
pending.push(when_undefined);
}
true
}
fn declarations_share_exhaustive_conditional_family(
&self,
analyzer: &CppGraphSource<'_>,
candidates: &[&CodeUnit],
) -> Option<(usize, usize)> {
let mut family_range = None;
for candidate in candidates {
let mut candidate_family = None;
for occurrence in analyzer.declaration_source_occurrences(candidate)? {
let key = occurrence.exhaustive_family?;
if candidate_family.is_some_and(|existing| existing != key) {
return None;
}
candidate_family = Some(key);
}
let candidate_family = candidate_family?;
if family_range.is_some_and(|existing| existing != candidate_family) {
return None;
}
family_range = Some(candidate_family);
}
family_range
}
pub fn complementary_same_fqn_type_declarations(
&self,
analyzer: &CppGraphSource<'_>,
candidates: &[&CodeUnit],
target: &CodeUnit,
) -> bool {
if candidates.len() < 2
|| !self.alternate_same_fqn_type_declarations(analyzer, candidates, target)
|| self
.declarations_share_exhaustive_conditional_family(analyzer, candidates)
.is_none()
{
return false;
}
Self::preprocessor_guard_terms_cover_all_paths(
&self.declaration_family_guard_terms(analyzer, candidates),
)
}
fn declaration_family_guard_terms(
&self,
analyzer: &CppGraphSource<'_>,
candidates: &[&CodeUnit],
) -> Vec<HashSet<PreprocessorGuard>> {
candidates
.iter()
.flat_map(|candidate| declaration_guard_requirements(analyzer, self.cpp, candidate))
.map(|(_, guards)| guards)
.collect()
}
fn exhaustive_guard_family_activation(
&self,
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
reference: &CallableReferenceContext<'_>,
) -> Option<usize> {
if !analyzer
.declaration_source_occurrences(candidate)?
.iter()
.any(|occurrence| occurrence.fact.callable_activation.is_some())
{
return None;
}
let family = self
.visible_identifier_candidates(reference.file, candidate.identifier())
.filter(|peer| {
peer.kind() == candidate.kind()
&& peer.fq_name() == candidate.fq_name()
&& peer.source() == candidate.source()
})
.collect::<Vec<_>>();
let (_, family_end) =
self.declarations_share_exhaustive_conditional_family(analyzer, &family)?;
if !Self::preprocessor_guard_terms_cover_all_paths(
&self.declaration_family_guard_terms(analyzer, &family),
) {
return None;
}
let effective = self.effective_reference_guards(reference.file, reference.guards());
if !declaration_guard_requirements(analyzer, self.cpp, candidate)
.iter()
.any(|(_, guards)| guards_compatible_at_reference(guards, effective.as_ref()))
{
return None;
}
(first_declaration_byte(analyzer, candidate)?
== family
.iter()
.filter_map(|peer| first_declaration_byte(analyzer, peer))
.min()?)
.then_some(family_end)
}
fn type_candidate_preserving_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidate: &CodeUnit,
target: &CodeUnit,
) -> Option<CodeUnit> {
let mut current = candidate.clone();
let mut matched_target = same_visible_symbol(¤t, target)
|| self.compatible_primary_template_redeclarations(¤t, target);
let mut seen = HashSet::default();
loop {
if !seen.insert(current.clone()) {
return None;
}
let Some(alias_target) = self.structured_alias_target(analyzer, ¤t) else {
return matched_target
.then(|| target.clone())
.or_else(|| current.is_class().then_some(current));
};
if self.flattened_macro_namespace_alias_target_matches(
analyzer,
visible_from,
¤t,
&alias_target,
target,
) {
return Some(target.clone());
}
if matches!(alias_target, StructuredAliasTarget::Builtin) {
return matched_target
.then(|| target.clone())
.or_else(|| current.is_class().then_some(current));
}
if !self.cpp_template_metadata.contains_key(¤t)
&& let Some(primary) =
self.resolve_structured_alias_primary(visible_from, ¤t, &alias_target)
&& (same_visible_symbol(&primary, target)
|| self.compatible_primary_template_redeclarations(&primary, target))
{
return Some(target.clone());
}
if same_visible_symbol(¤t, target) {
return Some(target.clone());
}
if self.cpp_template_metadata.contains_key(¤t) {
return None;
}
let Some(next) =
self.resolve_structured_alias_target(visible_from, ¤t, &alias_target)
else {
return matched_target.then(|| target.clone());
};
current = next;
matched_target |= same_visible_symbol(¤t, target)
|| self.compatible_primary_template_redeclarations(¤t, target);
}
}
fn compatible_primary_template_redeclarations(
&self,
left: &CodeUnit,
right: &CodeUnit,
) -> bool {
let (Some(left_metadata), Some(right_metadata)) = (
self.cpp_template_metadata.get(left),
self.cpp_template_metadata.get(right),
) else {
return false;
};
left_metadata.primary_fq_name == right_metadata.primary_fq_name
&& left_metadata.is_primary()
&& right_metadata.is_primary()
&& cpp_reconcile_primary_template_parameters(
&[(left, left_metadata), (right, right_metadata)],
right,
)
.is_some()
}
fn alias_candidate_may_preserve_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidate: &CodeUnit,
target: &CodeUnit,
) -> bool {
let mut pending = vec![candidate.clone()];
let mut seen = HashSet::default();
while let Some(current) = pending.pop() {
if same_visible_symbol(¤t, target)
|| self.compatible_primary_template_redeclarations(¤t, target)
{
return true;
}
if self.cpp_template_metadata.contains_key(¤t) {
return true;
}
let Some(alias_target) = self.structured_alias_target(analyzer, ¤t) else {
continue;
};
let StructuredAliasTarget::Named {
components,
global,
arguments,
} = alias_target
else {
continue;
};
if arguments.is_some() || !seen.insert(current.clone()) {
return true;
}
let qualified = components.join("::");
let next = if global {
unique_logical_type_candidate(self.type_candidates(visible_from, &qualified))
} else {
self.resolve_unique_type_for_declaration(visible_from, ¤t, &qualified)
};
let Some(next) = next else {
return true;
};
match self.conditional_alias_arms(analyzer, visible_from, &next) {
Some(arms) => pending.extend(arms),
None => pending.push(next),
}
}
false
}
fn alias_candidate_structurally_reaches_target(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
candidate: &CodeUnit,
target: &CodeUnit,
) -> bool {
let mut pending = vec![candidate.clone()];
let mut seen = HashSet::default();
while let Some(current) = pending.pop() {
if same_visible_symbol(¤t, target)
|| self.compatible_primary_template_redeclarations(¤t, target)
|| self
.cpp_template_metadata
.get(¤t)
.zip(self.cpp_template_metadata.get(target))
.is_some_and(|(current, target)| {
current.primary_fq_name == target.primary_fq_name
})
{
return true;
}
if !seen.insert(current.clone()) {
continue;
}
let Some(alias_target) = self.structured_alias_target(analyzer, ¤t) else {
continue;
};
if let StructuredAliasTarget::Named { components, .. } = &alias_target
&& components[..components.len().saturating_sub(1)]
.iter()
.any(|component| {
self.type_reference_component_directly_names_target(component, target)
})
&& self.structured_class_alias_path_preserves_target(
analyzer,
visible_from,
¤t,
target,
)
{
return true;
}
if matches!(alias_target, StructuredAliasTarget::Builtin) {
continue;
}
let Some(primary) =
self.resolve_structured_alias_primary(visible_from, ¤t, &alias_target)
else {
continue;
};
match self.conditional_alias_arms(analyzer, visible_from, &primary) {
Some(arms) => pending.extend(arms),
None => pending.push(primary),
}
}
false
}
pub fn qualified_alias_reference_may_reach_target(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
components: &[String],
global: bool,
target: &CodeUnit,
) -> bool {
for end in 0..components.len() {
let spelled = &components[..=end];
for candidate in self
.visible_identifier_candidates(file, &components[end])
.filter(|candidate| declared_type_alias(analyzer, candidate))
{
let candidate_components = canonical_cpp_scope_components(candidate);
let shape_matches = if global {
candidate_components == spelled
} else {
candidate_components.ends_with(spelled)
};
if shape_matches
&& self.alias_candidate_structurally_reaches_target(
analyzer, file, candidate, target,
)
{
return true;
}
}
}
false
}
fn type_candidates_for_declaration<'b>(
&'b self,
visible_from: &ProjectFile,
declaration: &CodeUnit,
raw_name: &str,
) -> Vec<&'b CodeUnit> {
let Some(normalized) = normalize_reference_name(raw_name) else {
return Vec::new();
};
if let Some(namespace) = cpp_namespace_for(declaration) {
for prefix in namespace_prefixes(&namespace) {
let qualified = format!("{prefix}::{normalized}");
let candidates = self.type_candidates(visible_from, &qualified);
if !candidates.is_empty() {
return candidates;
}
}
}
self.type_candidates(visible_from, &normalized)
}
fn resolve_unique_type_for_declaration(
&self,
visible_from: &ProjectFile,
declaration: &CodeUnit,
raw_name: &str,
) -> Option<CodeUnit> {
unique_logical_type_candidate(self.type_candidates_for_declaration(
visible_from,
declaration,
raw_name,
))
}
fn resolve_defining_type_for_declaration(
&self,
analyzer: &CppGraphSource<'_>,
visible_from: &ProjectFile,
declaration: &CodeUnit,
raw_name: &str,
) -> Option<CodeUnit> {
let candidates = self.type_candidates_for_declaration(visible_from, declaration, raw_name);
let logical = unique_logical_type_candidate(candidates.clone())?;
let mut defining = candidates.into_iter().filter(|candidate| {
candidate.is_class()
&& cpp_class_declaration_strength(analyzer, candidate)
== CppClassDeclarationStrength::Full
});
match (defining.next(), defining.next()) {
(Some(unique_definition), None) => Some(unique_definition.clone()),
_ => Some(logical),
}
}
pub fn resolves_to_type(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
raw_name: &str,
target: &CodeUnit,
) -> bool {
let Some(normalized) = normalize_reference_name(raw_name) else {
return false;
};
let candidates = self.type_candidates(file, &normalized);
if candidates.is_empty() {
return self.parser_alias_resolves_to_type(file, raw_name, target);
}
let Some(resolved) =
self.unique_type_candidate_preserving_target(analyzer, file, &candidates, target)
else {
return false;
};
same_symbol(&resolved, target) || same_visible_symbol(&resolved, target)
}
pub fn alias_target(&self, alias: &CodeUnit) -> Option<CodeUnit> {
let mut current = alias.clone();
let mut visited = HashSet::default();
while visited.insert(current.clone()) {
let raw_target = cpp_canonical_alias_target_text(&self.cpp_source(), ¤t)?;
let resolved =
self.resolve_type_for_declaration(current.source(), ¤t, &raw_target)?;
if resolved.is_class() {
return Some(resolved);
}
if !declared_type_alias(&self.cpp_source(), &resolved) {
return None;
}
current = resolved;
}
None
}
pub fn same_logical_callable(
&self,
analyzer: &CppGraphSource<'_>,
left: &CodeUnit,
right: &CodeUnit,
) -> bool {
if same_logical_symbol(left, right) {
return true;
}
if left.kind() != right.kind()
|| !left.is_callable()
|| !right.is_callable()
|| left.fq_name() != right.fq_name()
{
return false;
}
let (Some(left_comparable), Some(right_comparable)) = (
self.callable_comparable(analyzer, left),
self.callable_comparable(analyzer, right),
) else {
return false;
};
let binding = match (&left_comparable.template, &right_comparable.template) {
(Some(left_template), Some(right_template))
if left_template.is_templated() || right_template.is_templated() =>
{
match left_template.reconcile(right_template) {
CppTemplateReconciliation::Equivalent(binding) => binding,
CppTemplateReconciliation::Distinct
| CppTemplateReconciliation::Unsupported => return false,
}
}
(Some(_), Some(_)) => CppTemplateBinding::default(),
_ if self.callable_is_template_declaration(analyzer, left)
|| self.callable_is_template_declaration(analyzer, right) =>
{
return false;
}
_ => CppTemplateBinding::default(),
};
if left_comparable.suffix != right_comparable.suffix
|| left_comparable.shapes.len() != right_comparable.shapes.len()
{
return false;
}
left_comparable
.shapes
.iter()
.zip(right_comparable.shapes.iter())
.all(|(left_slot, right_slot)| match (left_slot, right_slot) {
(CppComparableSlot::Ellipsis, CppComparableSlot::Ellipsis) => true,
(CppComparableSlot::Shape(left_shape), CppComparableSlot::Shape(right_shape)) => {
self.comparable_shapes_agree(analyzer, &binding, left_shape, right_shape)
}
_ => false,
})
}
pub fn same_logical_class_definition(
&self,
analyzer: &CppGraphSource<'_>,
left: &CodeUnit,
right: &CodeUnit,
) -> bool {
if same_symbol(left, right) {
return true;
}
if !left.is_class() || !right.is_class() || left.fq_name() != right.fq_name() {
return false;
}
let key = if left <= right {
(left.clone(), right.clone())
} else {
(right.clone(), left.clone())
};
visibility_memo_value(&self.class_definition_agreements, &self.reads, key, || {
self.class_definitions_agree(analyzer, left, right)
})
}
fn class_definitions_agree(
&self,
analyzer: &CppGraphSource<'_>,
left: &CodeUnit,
right: &CodeUnit,
) -> bool {
let complete_definition = |class: &CodeUnit| {
let mut facts = analyzer
.declaration_source_properties(class)?
.into_iter()
.filter(|fact| fact.class_strength == CppClassDeclarationStrength::Full);
let fact = facts.next()?;
facts.next().is_none().then_some(fact)
};
let (Some(left_fact), Some(right_fact)) =
(complete_definition(left), complete_definition(right))
else {
return false;
};
if left_fact.bases != right_fact.bases {
return false;
}
let (Some(left_template), Some(right_template)) = (
self.declaration_template_context(left, left_fact.declaration),
self.declaration_template_context(right, right_fact.declaration),
) else {
return false;
};
if !matches!(
left_template.reconcile(&right_template),
CppTemplateReconciliation::Equivalent(_)
) {
return false;
}
let members = |class: &CodeUnit| {
let mut members = analyzer
.direct_children(class)
.into_iter()
.filter(|member| member.source() == class.source())
.map(|member| {
(
member.kind(),
member.identifier().to_owned(),
member.signature().map(str::to_owned),
)
})
.collect::<Vec<_>>();
members.sort();
members
};
members(left) == members(right)
}
pub fn class_definition_bridges_to_file(
&self,
analyzer: &CppGraphSource<'_>,
class: &CodeUnit,
file: &ProjectFile,
header_body_related: &dyn Fn(&ProjectFile, &ProjectFile) -> bool,
) -> bool {
class.is_class()
&& class.source() != file
&& crate::identity::cpp_source_path_is_header(class.source())
&& analyzer
.index
.definitions_by_structured_name(class.fq(), Language::Cpp)
.into_iter()
.any(|copy| {
copy.is_class()
&& copy.source() != class.source()
&& copy.source() != file
&& crate::identity::cpp_source_path_is_header(copy.source())
&& header_body_related(copy.source(), file)
&& self.same_logical_class_definition(analyzer, class, ©)
})
}
pub fn member_declaration_bridges_to_body(
&self,
analyzer: &CppGraphSource<'_>,
declaration: &CodeUnit,
body: &CodeUnit,
header_body_related: &dyn Fn(&ProjectFile, &ProjectFile) -> bool,
) -> bool {
declaration.is_callable()
&& self
.cpp
.structural_parent_of(declaration)
.is_some_and(|owner| {
self.class_definition_bridges_to_file(
analyzer,
&owner,
body.source(),
header_body_related,
)
})
}
fn comparable_shapes_agree(
&self,
analyzer: &CppGraphSource<'_>,
binding: &CppTemplateBinding,
left: &CppComparableParameter,
right: &CppComparableParameter,
) -> bool {
let mut stack = vec![(left.root(), right.root())];
while let Some((left_index, right_index)) = stack.pop() {
match (left.node(left_index), right.node(right_index)) {
(
CppComparableNode::Named {
name: left_name,
primitive: left_primitive,
konst: left_konst,
volatil: left_volatil,
},
CppComparableNode::Named {
name: right_name,
primitive: right_primitive,
konst: right_konst,
volatil: right_volatil,
},
) => {
if left_konst != right_konst
|| left_volatil != right_volatil
|| left_primitive != right_primitive
|| !self.comparable_names_agree(
analyzer,
binding,
left_name,
right_name,
*left_primitive,
)
{
return false;
}
}
(
CppComparableNode::Pointer {
inner: left_inner,
konst: left_konst,
volatil: left_volatil,
},
CppComparableNode::Pointer {
inner: right_inner,
konst: right_konst,
volatil: right_volatil,
},
) => {
if left_konst != right_konst || left_volatil != right_volatil {
return false;
}
stack.push((*left_inner, *right_inner));
}
(
CppComparableNode::Reference { inner: left_inner },
CppComparableNode::Reference { inner: right_inner },
)
| (
CppComparableNode::Array { inner: left_inner },
CppComparableNode::Array { inner: right_inner },
) => stack.push((*left_inner, *right_inner)),
(
CppComparableNode::Generic {
base: left_base,
arguments: left_arguments,
},
CppComparableNode::Generic {
base: right_base,
arguments: right_arguments,
},
) => {
if left_arguments.len() != right_arguments.len() {
return false;
}
stack.push((*left_base, *right_base));
stack.extend(
left_arguments.iter().zip(right_arguments.iter()).map(
|(left_argument, right_argument)| (*left_argument, *right_argument),
),
);
}
_ => return false,
}
}
true
}
fn comparable_names_agree(
&self,
analyzer: &CppGraphSource<'_>,
binding: &CppTemplateBinding,
left: &StructuredTypeName,
right: &StructuredTypeName,
primitive: bool,
) -> bool {
if primitive {
return left.path() == right.path();
}
if !binding.is_empty() {
let (Some(left_head), Some(right_head)) = (left.path().first(), right.path().first())
else {
return false;
};
match (
binding.is_left_parameter(left_head),
binding.is_right_parameter(right_head),
) {
(true, true) => {
return !left.is_absolute()
&& !right.is_absolute()
&& binding.binds(right_head, left_head)
&& left.path()[1..] == right.path()[1..];
}
(false, false) => {}
_ => return false,
}
}
match (
self.comparable_name_terminal(analyzer, left),
self.comparable_name_terminal(analyzer, right),
) {
(Some(left_terminal), Some(right_terminal)) => {
same_logical_symbol(&left_terminal, &right_terminal)
}
(None, None) => {
left.path() == right.path() && left.is_absolute() == right.is_absolute()
}
_ => false,
}
}
fn comparable_name_terminal(
&self,
analyzer: &CppGraphSource<'_>,
name: &StructuredTypeName,
) -> Option<CodeUnit> {
let mut current = self.comparable_name_declaration(analyzer, name)?;
let mut visited = HashSet::default();
for _ in 0..MAX_COMPARABLE_ALIAS_HOPS {
if !declared_type_alias(analyzer, ¤t) {
return current.is_class().then_some(current);
}
if !visited.insert(current.clone()) {
return None;
}
let facts = analyzer.declaration_source_properties(¤t)?;
if facts.iter().any(|fact| fact.adds_indirection) {
return None;
}
let raw_target = cpp_canonical_alias_target_text(analyzer, ¤t)?;
current = self.comparable_alias_target(analyzer, ¤t, &raw_target)?;
}
None
}
fn comparable_alias_target(
&self,
analyzer: &CppGraphSource<'_>,
alias: &CodeUnit,
raw_target: &str,
) -> Option<CodeUnit> {
let absolute = raw_target.trim_start().starts_with("::");
let normalized = normalize_reference_name(raw_target)?;
let path = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
&normalized,
);
let lexical_scope = cpp_namespace_for(alias).map_or_else(Vec::new, |namespace| {
brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
&namespace,
)
});
let name = StructuredTypeName::new(path, lexical_scope, absolute)?;
self.comparable_name_declaration(analyzer, &name)
}
fn comparable_name_declaration(
&self,
analyzer: &CppGraphSource<'_>,
name: &StructuredTypeName,
) -> Option<CodeUnit> {
visibility_memo_value(
&self.comparable_name_declarations,
&self.reads,
name.clone(),
|| {
let interner = segment_interner();
let identifier = name.path().last()?;
let candidates_by_identifier =
self.cpp.visibility_identifier_candidates(identifier);
let first_depth = if name.is_absolute() {
0
} else {
name.lexical_scope().len()
};
let mut resolved = None;
for depth in (0..=first_depth).rev() {
let mut structured = FqName::new();
for component in name.lexical_scope()[..depth].iter().chain(name.path()) {
structured.push(interner.intern(component, SegmentKind::Unknown));
}
let mut candidates = candidates_by_identifier
.iter()
.filter(|unit| unit.fq().same_segment_texts(&structured))
.filter(|unit| {
unit.kind() == CodeUnitType::Class
|| declared_type_alias(analyzer, unit)
})
.cloned();
let Some(first) = candidates.next() else {
continue;
};
resolved = candidates
.all(|unit| same_logical_symbol(&unit, &first))
.then_some(first);
break;
}
resolved
},
)
}
fn callable_comparable(
&self,
analyzer: &CppGraphSource<'_>,
unit: &CodeUnit,
) -> Option<Arc<ExtractedComparable>> {
visibility_memo_value(
&self.callable_comparables,
&self.reads,
unit.clone(),
|| {
self.extract_callable_comparable(analyzer, unit)
.map(Arc::new)
},
)
}
fn extract_callable_comparable(
&self,
analyzer: &CppGraphSource<'_>,
unit: &CodeUnit,
) -> Option<ExtractedComparable> {
let fact = analyzer
.declaration_source_properties(unit)?
.into_iter()
.find(|fact| fact.callable_comparable_shapes.is_some())?;
let template = self.declaration_template_context(unit, fact.declaration);
Some(ExtractedComparable {
shapes: fact.callable_comparable_shapes?,
suffix: fact.callable_identity_suffix?,
template,
})
}
pub fn declaration_template_context(
&self,
unit: &CodeUnit,
declaration: SourceDeclarationId,
) -> Option<CppTemplateContext> {
let facts = self
.cpp
.declaration_source_facts(self.token, unit.source())?;
let occurrence = facts.source.declaration(declaration).occurrence;
let range = facts.source.occurrence(occurrence).range;
let syntax = self.cpp.prepared_syntax(self.token, unit.source())?;
let mut node = node_for_exact_range(syntax.tree().root_node(), &range)?;
while node.kind() == "template_declaration" {
node = node.named_child(node.named_child_count().checked_sub(1)?)?;
}
cpp_declaration_template_context(node, syntax.source())
}
pub fn canonical_type_for_reference(
&self,
file: &ProjectFile,
raw_name: &str,
) -> Option<CodeUnit> {
let resolved = self.resolve_type(file, raw_name)?;
self.alias_target(&resolved).or(Some(resolved))
}
pub fn parser_alias_resolves_to_type(
&self,
file: &ProjectFile,
raw_name: &str,
target: &CodeUnit,
) -> bool {
let Some(alias_name) = normalize_reference_name(raw_name) else {
return false;
};
any_visible_source_file_matches(&self.visible_source_files_by_root, file, |source_file| {
self.file_alias_matches(self.cpp, source_file, &alias_name, target)
})
}
fn file_alias_matches(
&self,
cpp: &dyn CppSource,
file: &ProjectFile,
alias_name: &str,
target: &CodeUnit,
) -> bool {
self.canonical_alias_cell(cpp, file).is_some_and(|cell| {
cell.get()
.expect("published canonical aliases")
.iter()
.any(|alias| alias.name == alias_name && alias_target_matches_target(alias, target))
})
}
fn canonical_alias_cell(
&self,
cpp: &dyn CppSource,
file: &ProjectFile,
) -> Option<Arc<OnceLock<Box<[CppAlias]>>>> {
try_visibility_memo_value(&self.alias_cells, &self.reads, file.clone(), || {
let aliases = aliases_from_canonical_source(cpp, self.token, file).ok_or(())?;
self.parser_alias_source_parses
.fetch_add(1, Ordering::Relaxed);
#[cfg(any(test, feature = "test-support"))]
{
*self
.alias_source_parse_counts
.lock()
.expect("alias source parse count lock")
.entry(file.clone())
.or_default() += 1;
}
let cell = Arc::new(OnceLock::new());
cell.get_or_init(|| aliases.into_boxed_slice());
Ok::<_, ()>(cell)
})
.ok()
}
#[cfg(any(test, feature = "test-support"))]
pub fn visible_source_files_for_test(&self, file: &ProjectFile) -> HashSet<ProjectFile> {
self.visible_source_files_by_root
.get(file)
.cloned()
.unwrap_or_else(|| HashSet::from_iter([file.clone()]))
}
#[cfg(any(test, feature = "test-support"))]
pub fn alias_source_parse_count_for_test(&self, file: &ProjectFile) -> usize {
self.alias_source_parse_counts
.lock()
.expect("alias source parse count lock")
.get(file)
.copied()
.unwrap_or(0)
}
#[cfg(any(test, feature = "test-support"))]
pub fn parser_alias_fallback_file_count_for_test(&self) -> usize {
self.parser_alias_fallback_files.load(Ordering::Relaxed)
}
pub fn resolve_named(
&self,
file: &ProjectFile,
raw_name: &str,
kind: TargetKind,
) -> Option<CodeUnit> {
let normalized = normalize_reference_name(raw_name)?;
self.named_candidates_for_normalized(file, &normalized, kind)
.into_iter()
.next()
.cloned()
}
pub fn contains_named_symbol(
&self,
file: &ProjectFile,
raw_name: &str,
kind: TargetKind,
target: &CodeUnit,
) -> bool {
let Some(normalized) = normalize_reference_name(raw_name) else {
return false;
};
self.named_candidates_for_normalized(file, &normalized, kind)
.into_iter()
.any(|unit| {
matches_kind_for_lookup(unit, kind)
&& reference_matches_unit(&normalized, unit)
&& same_visible_symbol(unit, target)
})
}
pub fn named_candidates(
&self,
file: &ProjectFile,
raw_name: &str,
kind: TargetKind,
) -> Vec<CodeUnit> {
let Some(normalized) = normalize_reference_name(raw_name) else {
return Vec::new();
};
self.named_candidates_for_normalized(file, &normalized, kind)
.into_iter()
.cloned()
.collect()
}
pub fn resolve_known_non_target(
&self,
file: &ProjectFile,
raw_name: &str,
kind: TargetKind,
target: &CodeUnit,
) -> bool {
let Some(normalized) = normalize_reference_name(raw_name) else {
return false;
};
normalized.contains("::")
&& self
.named_candidates_for_normalized(file, &normalized, kind)
.into_iter()
.any(|unit| {
matches_kind_for_lookup(unit, kind)
&& reference_matches_unit(&normalized, unit)
&& !same_visible_symbol(unit, target)
})
}
pub fn resolve_call_return_binding(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
raw_name: &str,
arity: usize,
lexical_namespace: Option<&str>,
direct_type: Option<&CodeUnit>,
) -> Option<CppScanBinding> {
let normalized = normalize_reference_name(raw_name)?;
let mut candidates = Vec::new();
for function in
self.named_candidates_for_normalized(file, &normalized, TargetKind::FreeFunction)
{
if cpp_callable_arity(analyzer, function).is_none_or(|known| known.accepts(arity))
&& !direct_type.is_some_and(|direct_type| {
self.callable_is_constructor_declaration(analyzer, function)
&& type_owner_of(analyzer, function)
.is_some_and(|owner| same_visible_symbol(&owner, direct_type))
})
{
candidates.push(function.clone());
}
}
candidates = nearest_namespace_candidates(candidates, &normalized, lexical_namespace);
unanimous_return_binding(analyzer, self, file, &candidates)
}
pub fn resolve_call_return_binding_without_arity(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
raw_name: &str,
lexical_namespace: Option<&str>,
direct_type: Option<&CodeUnit>,
) -> (bool, Option<CppScanBinding>) {
let Some(normalized) = normalize_reference_name(raw_name) else {
return (false, None);
};
let mut candidates = self
.named_candidates_for_normalized(file, &normalized, TargetKind::FreeFunction)
.into_iter()
.filter(|function| {
function.is_function()
&& !direct_type.is_some_and(|direct_type| {
self.callable_is_constructor_declaration(analyzer, function)
&& type_owner_of(analyzer, function)
.is_some_and(|owner| same_visible_symbol(&owner, direct_type))
})
})
.cloned()
.collect::<Vec<_>>();
candidates = nearest_namespace_candidates(candidates, &normalized, lexical_namespace);
let has_candidates = !candidates.is_empty();
(
has_candidates,
unanimous_return_binding(analyzer, self, file, &candidates),
)
}
pub fn visible_identifier_candidates<'b>(
&'b self,
file: &ProjectFile,
identifier: &str,
) -> impl Iterator<Item = &'b CodeUnit> + 'b {
self.visible_by_identifier
.get(file)
.and_then(|by_name| by_name.get(identifier))
.into_iter()
.flatten()
}
pub fn type_reference_component_directly_names_target(
&self,
component: &str,
target: &CodeUnit,
) -> bool {
component == target.identifier()
|| self
.cpp_template_metadata
.get(target)
.is_some_and(|metadata| component == metadata.primary_name)
}
pub fn visible_type_reference_component_names_for_target(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
target: &CodeUnit,
) -> HashSet<String> {
let mut names = HashSet::from_iter([target.identifier().to_string()]);
if let Some(metadata) = self.cpp_template_metadata.get(target) {
names.insert(metadata.primary_name.clone());
}
if let Some(by_identifier) = self.visible_by_identifier.get(file) {
for (identifier, candidates) in by_identifier {
if candidates.iter().any(|candidate| {
(candidate.is_class()
&& (same_visible_symbol(candidate, target)
|| self.compatible_primary_template_redeclarations(candidate, target)))
|| (declared_type_alias(analyzer, candidate)
&& self.alias_candidate_structurally_reaches_target(
analyzer, file, candidate, target,
))
}) {
names.insert(identifier.clone());
}
}
}
names
}
pub fn indexed_structural_class_scope(
&self,
file: &ProjectFile,
class: Node<'_>,
source: &str,
) -> Option<Vec<String>> {
let key = (file.clone(), class.start_byte(), class.end_byte());
visibility_memo_value(
&self.indexed_structural_class_scopes,
&self.reads,
key,
|| {
let name = class.child_by_field_name("name")?;
let identifier = if name.kind() == "template_type" {
node_text(name.child_by_field_name("name")?, source).to_string()
} else {
let mut components = Vec::new();
append_cpp_name_components(name, source, &mut components)?;
components.last()?.clone()
};
let visible = self
.visible_identifier_candidates(file, &identifier)
.cloned()
.collect::<Vec<_>>();
let mut visible = visible;
for candidate in
self.visible_by_file
.get(file)
.into_iter()
.flatten()
.filter(|candidate| {
self.cpp_template_metadata
.get(candidate)
.is_some_and(|metadata| metadata.primary_name == identifier)
})
{
if !visible
.iter()
.any(|existing| same_logical_symbol(existing, candidate))
{
visible.push(candidate.clone());
}
}
let cpp_source = self.cpp_source();
let candidates = visible
.iter()
.filter(|candidate| {
candidate.source() == file
&& candidate.is_class()
&& !declared_type_alias(&cpp_source, candidate)
&& self.cpp.ranges(candidate).iter().any(|range| {
range.start_byte <= class.start_byte()
&& class.end_byte() <= range.end_byte
})
})
.collect::<Vec<_>>();
let owner = if name.kind() == "template_type" {
let expected = normalize_cpp_whitespace(node_text(name, source));
let interner = brokk_bifrost_core::analyzer::fq_name::segment_interner();
let exact = candidates
.iter()
.copied()
.filter(|candidate| {
candidate
.fq()
.segments()
.iter()
.rev()
.find_map(|&segment| {
let (text, kind) = interner.resolve(segment);
matches!(
kind,
brokk_bifrost_core::analyzer::fq_name::SegmentKind::Type
| brokk_bifrost_core::analyzer::fq_name::SegmentKind::Nested
)
.then_some(text)
})
.is_some_and(|text| text == expected)
})
.collect::<Vec<_>>();
unique_logical_type_candidate(exact)
.or_else(|| unique_logical_type_candidate(candidates.clone()))?
} else {
unique_logical_type_candidate(candidates)?
};
Some(canonical_cpp_scope_components(&owner))
},
)
}
pub fn indexed_enclosing_owner_scope(
&self,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
node: Node<'_>,
) -> Option<Vec<String>> {
let anchor = std::iter::successors(Some(node), |current| current.parent())
.find(|current| {
matches!(
current.kind(),
"function_definition"
| "class_specifier"
| "struct_specifier"
| "union_specifier"
)
})
.unwrap_or(node);
let key = (file.clone(), anchor.start_byte(), anchor.end_byte());
visibility_memo_value(
&self.indexed_enclosing_owner_scopes,
&self.reads,
key,
|| {
let range = Range {
start_byte: node.start_byte(),
end_byte: node.end_byte(),
start_line: node.start_position().row,
end_line: node.end_position().row,
};
let start = analyzer.enclosing_code_unit(file, &range)?;
let owner = brokk_bifrost_core::analyzer::usages::common::enclosing_owner_chain(
start,
|unit| self.cached_precise_parent_of(analyzer, unit),
)
.find(|unit| {
unit.is_class()
&& !analyzer
.type_alias_provider()
.is_some_and(|provider| provider.is_type_alias(unit))
})?;
Some(canonical_cpp_scope_components(&owner))
},
)
}
fn cached_precise_parent_of(
&self,
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
) -> Option<CodeUnit> {
visibility_memo_value(
&self.precise_parent_cache,
&self.reads,
code_unit.clone(),
|| precise_parent_resolution(analyzer, code_unit).map(|owner| owner.unit),
)
}
pub fn callable_is_constructor_declaration(
&self,
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> bool {
if !candidate.is_function() {
return false;
}
if analyzer
.declaration_source_properties(candidate)
.is_some_and(|facts| facts.iter().any(|fact| fact.callable_is_constructor))
{
return true;
}
let metadata = analyzer.signature_metadata(candidate);
!metadata.is_empty()
&& metadata
.iter()
.all(|signature| signature.return_type_text().is_none())
}
pub fn callable_is_deduction_guide_declaration(
&self,
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> bool {
callable_declaration_has_fact(analyzer, candidate, |fact| fact.callable_is_deduction_guide)
}
pub fn callable_is_template_declaration(
&self,
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> bool {
callable_declaration_has_fact(analyzer, candidate, |fact| fact.callable_is_template)
}
pub fn type_name_candidates<'b>(
&'b self,
file: &ProjectFile,
normalized: &str,
) -> Vec<&'b CodeUnit> {
self.candidate_units(file, normalized, TargetKind::Type)
}
pub fn visible_members_for_owner_name<'b>(
&'b self,
file: &ProjectFile,
owner: &CodeUnit,
name: &str,
) -> Vec<&'b CodeUnit> {
self.visible_identifier_candidates(file, name)
.filter(|unit| {
brokk_bifrost_core::analyzer::default_parent_fq_name(unit)
.is_some_and(|parent| parent == owner.fq_name())
})
.collect()
}
pub fn visible_member_for_owner_name(
&self,
file: &ProjectFile,
owner: &CodeUnit,
name: &str,
) -> VisibleMemberResolution {
let candidates = self.visible_members_for_owner_name(file, owner, name);
let mut callables = Vec::new();
let mut non_callable = None;
for candidate in candidates {
if candidate.is_function() {
callables.push(candidate.clone());
} else if non_callable.is_none() {
non_callable = Some(candidate.clone());
}
}
match (callables.is_empty(), non_callable) {
(false, None) => VisibleMemberResolution::Callable(callables),
(true, Some(_)) => VisibleMemberResolution::NonCallable,
(false, Some(_)) => VisibleMemberResolution::AmbiguousKind,
(true, None) => VisibleMemberResolution::Missing,
}
}
fn field_declared_type_fact(
&self,
analyzer: &CppGraphSource<'_>,
field: &CodeUnit,
) -> Option<DeclaredFieldTypeFact> {
visibility_memo_value(&self.field_type_facts, &self.reads, field.clone(), || {
decode_field_declared_type_fact(analyzer, field)
})
}
fn structured_alias_target(
&self,
analyzer: &CppGraphSource<'_>,
unit: &CodeUnit,
) -> Option<StructuredAliasTarget> {
visibility_memo_value(
&self.structured_alias_targets,
&self.reads,
unit.clone(),
|| decode_structured_alias_target(analyzer, unit),
)
}
pub fn type_candidates<'b>(
&'b self,
file: &ProjectFile,
normalized: &str,
) -> Vec<&'b CodeUnit> {
let qualified = normalized.contains("::");
let mut candidates = self
.candidate_units(file, normalized, TargetKind::Type)
.into_iter()
.filter(|unit| {
unit.kind() == CodeUnitType::Class || declared_type_alias(&self.cpp_source(), unit)
})
.filter(|unit| qualified || !is_function_local_unit(unit))
.collect::<Vec<_>>();
dedup_unit_refs(&mut candidates);
candidates
}
pub fn named_candidates_for_normalized<'b>(
&'b self,
file: &ProjectFile,
normalized: &str,
kind: TargetKind,
) -> Vec<&'b CodeUnit> {
let mut candidates = self
.candidate_units(file, normalized, kind)
.into_iter()
.filter(|unit| {
matches_kind_for_lookup(unit, kind) && reference_matches_unit(normalized, unit)
})
.collect::<Vec<_>>();
dedup_unit_refs(&mut candidates);
candidates
}
pub fn candidate_units<'b>(
&'b self,
file: &ProjectFile,
normalized: &str,
kind: TargetKind,
) -> Vec<&'b CodeUnit> {
if normalized.contains("::") {
let parts = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
normalized,
);
let Some(identifier) = parts.last() else {
return Vec::new();
};
let fqns = cpp_reference_fqn_candidates_from_parts(&parts, kind);
return self
.visible_identifier_candidates(file, identifier)
.filter(|unit| {
#[cfg(any(test, feature = "test-support"))]
self.qualified_candidate_inspections
.fetch_add(1, Ordering::Relaxed);
fqns.iter().any(|fqn| unit.fq_name() == *fqn)
|| canonical_cpp_name_matches(unit, normalized)
})
.collect();
}
self.visible_identifier_candidates(file, normalized)
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn reset_qualified_candidate_inspections(&self) {
self.qualified_candidate_inspections
.store(0, Ordering::Relaxed);
}
#[cfg(any(test, feature = "test-support"))]
pub fn qualified_candidate_inspections(&self) -> usize {
self.qualified_candidate_inspections.load(Ordering::Relaxed)
}
#[cfg(any(test, feature = "test-support"))]
pub fn visibility_identifier_lookup_count(&self) -> usize {
self.visibility_identifier_lookup_count
}
#[cfg(any(test, feature = "test-support"))]
pub fn visibility_identifier_batch_count(&self) -> usize {
self.visibility_identifier_batch_count
}
#[cfg(any(test, feature = "test-support"))]
pub fn reset_target_preserving_type_resolution_count(&self) {
self.target_preserving_type_resolution_count
.store(0, Ordering::Relaxed);
}
#[cfg(any(test, feature = "test-support"))]
pub fn target_preserving_type_resolution_count(&self) -> usize {
self.target_preserving_type_resolution_count
.load(Ordering::Relaxed)
}
#[cfg(any(test, feature = "test-support"))]
pub fn visible_parser_alias_name_set_build_count(&self) -> usize {
self.visible_parser_alias_name_set_build_count
.load(Ordering::Relaxed)
}
}
fn callable_declaration_has_fact(
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
matches: impl Fn(&brokk_bifrost_core::analyzer::cpp_facts::CppDeclarationSourceFact) -> bool,
) -> bool {
candidate.is_function()
&& analyzer
.declaration_source_properties(candidate)
.is_some_and(|facts| facts.iter().any(matches))
}
#[derive(Default)]
struct IncludeGraph {
targets_by_file: HashMap<ProjectFile, Vec<ProjectFile>>,
}
impl IncludeGraph {
fn extend_with<F>(
&mut self,
root: &ProjectFile,
cancellation: Option<&CancellationToken>,
targets_for: &mut F,
) where
F: FnMut(&ProjectFile) -> Vec<ProjectFile>,
{
let mut stack = vec![root.clone()];
while let Some(file) = stack.pop() {
if cancellation.is_some_and(CancellationToken::is_cancelled) {
break;
}
if self.targets_by_file.contains_key(&file) {
continue;
}
let targets = targets_for(&file);
stack.extend(targets.iter().cloned());
self.targets_by_file.insert(file, targets);
}
}
fn files(&self) -> impl Iterator<Item = &ProjectFile> {
self.targets_by_file.keys()
}
fn targets(&self, file: &ProjectFile) -> &[ProjectFile] {
self.targets_by_file
.get(file)
.map(Vec::as_slice)
.unwrap_or_default()
}
fn reachable_files(
&self,
root: &ProjectFile,
cancellation: Option<&CancellationToken>,
) -> HashSet<ProjectFile> {
let mut pending = vec![root.clone()];
let mut visited = HashSet::default();
while let Some(file) = pending.pop() {
if cancellation.is_some_and(CancellationToken::is_cancelled) {
break;
}
if visited.insert(file.clone()) {
pending.extend(self.targets(&file).iter().cloned());
}
}
visited
}
}
fn build_bounded_visible_declarations(
cpp: &dyn CppSource,
token: QueryToken<'_>,
analyzer: &CppGraphSource<'_>,
roots: &HashSet<ProjectFile>,
visible_sources: &HashMap<ProjectFile, HashSet<ProjectFile>>,
cancellation: Option<&CancellationToken>,
stats: &mut BoundedVisibilityStats,
) -> HashMap<ProjectFile, HashSet<CodeUnit>> {
let mut candidates_by_identifier = HashMap::default();
let mut declarations_by_source_and_reading: HashMap<
(ProjectFile, bool),
BoundedVisibilityDeclarations,
> = HashMap::default();
let mut dependency_names_by_unit: HashMap<CodeUnit, HashSet<String>> = HashMap::default();
roots
.iter()
.map(|root| {
let reading_is_c = analyzer.reference_uses_c_semantics(root);
let root_declarations = declarations_by_source_and_reading
.entry((root.clone(), reading_is_c))
.or_insert_with(|| {
bounded_visibility_declarations(cpp, analyzer, root, reading_is_c, stats)
});
let mut visible = root_declarations
.all
.iter()
.cloned()
.collect::<HashSet<_>>();
let mut pending_names = HashSet::default();
if let Some(prepared) = cpp.prepared_syntax(token, root) {
let mut cursor = prepared.tree().walk();
let mut pending_nodes = vec![prepared.tree().root_node()];
while let Some(node) = pending_nodes.pop() {
if matches!(
node.kind(),
"identifier"
| "type_identifier"
| "field_identifier"
| "namespace_identifier"
) {
pending_names.insert(node_text(node, prepared.source()).to_string());
}
if node.kind() == "preproc_arg" {
for reference in
object_macro_replacement_type_references(node, prepared.source())
{
pending_names.extend(reference.components);
}
}
pending_nodes.extend(node.named_children(&mut cursor));
}
}
stats.root_names += pending_names.len();
let mut completed_names = HashSet::default();
while !pending_names.is_empty() {
stats.rounds += 1;
let round_names = std::mem::take(&mut pending_names);
let mut requested_names_by_source: HashMap<ProjectFile, HashSet<String>> =
HashMap::default();
let mut identifiers = Vec::new();
for identifier in round_names {
if !completed_names.insert(identifier.clone())
|| cancellation.is_some_and(CancellationToken::is_cancelled)
{
continue;
}
identifiers.push(identifier);
}
let missing_identifiers = identifiers
.iter()
.filter(|identifier| !candidates_by_identifier.contains_key(*identifier))
.cloned()
.collect::<HashSet<_>>();
if !missing_identifiers.is_empty() {
let lookup_started = Instant::now();
let mut candidates = cpp
.visibility_identifier_candidates_batch(&missing_identifiers, cancellation);
stats.lookup_elapsed += lookup_started.elapsed();
stats.identifier_lookups += missing_identifiers.len();
stats.identifier_batches += 1;
if cancellation.is_some_and(CancellationToken::is_cancelled) {
break;
}
for identifier in missing_identifiers {
let units = candidates.remove(&identifier).unwrap_or_default();
let candidate_count = units.len();
let candidate_sources = units
.into_iter()
.map(|unit| unit.source().clone())
.collect::<HashSet<_>>();
candidates_by_identifier
.insert(identifier, (candidate_sources, candidate_count));
}
}
for identifier in identifiers {
let (candidate_sources, candidate_count) = candidates_by_identifier
.get(&identifier)
.expect("every missing identifier was inserted after the batch lookup");
stats.candidate_units += *candidate_count;
for source in candidate_sources.iter().cloned() {
if source != *root
&& visible_sources
.get(root)
.is_some_and(|files| files.contains(&source))
{
requested_names_by_source
.entry(source)
.or_default()
.insert(identifier.clone());
}
}
}
stats.candidate_sources += requested_names_by_source.len();
for (source, requested_names) in requested_names_by_source {
let declarations = declarations_by_source_and_reading
.entry((source.clone(), reading_is_c))
.or_insert_with(|| {
bounded_visibility_declarations(
cpp,
analyzer,
&source,
reading_is_c,
stats,
)
});
let mut selected = HashSet::default();
for name in requested_names {
if let Some(units) = declarations.by_identifier.get(&name) {
selected.extend(units.iter().cloned());
}
}
for unit in selected {
stats.selected_units += 1;
let dependency_names = dependency_names_by_unit
.entry(unit.clone())
.or_insert_with(|| {
let facts_started = Instant::now();
let mut dependency_names = HashSet::default();
if let Some(facts) = analyzer.declaration_source_properties(&unit) {
for fact in facts {
for name in fact.dependency_type_names {
stats.dependency_source_names += 1;
#[cfg(any(test, feature = "test-support"))]
BOUNDED_VISIBILITY_DEPENDENCY_AST_NODE_COUNT
.with(|count| count.set(count.get() + 1));
dependency_names.insert(name);
}
}
stats.dependency_source_elapsed += facts_started.elapsed();
}
dependency_names
});
for name in dependency_names.iter() {
if !completed_names.contains(name) && pending_names.insert(name.clone())
{
stats.dependency_names += 1;
}
}
visible.insert(unit);
}
}
}
(root.clone(), visible)
})
.collect()
}
struct BoundedVisibilityDeclarations {
all: Vec<CodeUnit>,
by_identifier: HashMap<String, Vec<CodeUnit>>,
}
fn bounded_visibility_declarations(
cpp: &dyn CppSource,
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
c_semantics: bool,
stats: &mut BoundedVisibilityStats,
) -> BoundedVisibilityDeclarations {
let declarations_started = Instant::now();
let all = bounded_visibility_declarations_in_reading(analyzer, file, c_semantics)
.into_iter()
.collect::<Vec<_>>();
stats.declaration_elapsed += declarations_started.elapsed();
stats.declaration_reads += 1;
stats.declaration_units += all.len();
let mut by_identifier: HashMap<String, Vec<CodeUnit>> = HashMap::default();
for unit in &all {
by_identifier
.entry(unit.identifier().to_string())
.or_default()
.push(unit.clone());
if unit.is_class()
&& let Some(metadata) = cpp.template_metadata(unit)
&& metadata.primary_name != unit.identifier()
{
by_identifier
.entry(metadata.primary_name)
.or_default()
.push(unit.clone());
}
}
BoundedVisibilityDeclarations { all, by_identifier }
}
#[derive(Default)]
struct BoundedVisibilityStats {
rounds: usize,
root_names: usize,
identifier_lookups: usize,
identifier_batches: usize,
candidate_units: usize,
candidate_sources: usize,
declaration_reads: usize,
declaration_units: usize,
selected_units: usize,
dependency_source_names: usize,
dependency_names: usize,
lookup_elapsed: Duration,
declaration_elapsed: Duration,
dependency_source_elapsed: Duration,
}
fn bounded_visibility_declarations_in_reading(
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
c_semantics: bool,
) -> BTreeSet<CodeUnit> {
#[cfg(any(test, feature = "test-support"))]
BOUNDED_VISIBILITY_DECLARATION_READ_COUNT.with(|count| count.set(count.get() + 1));
analyzer.declarations_in_reading(file, c_semantics)
}
#[cfg(any(test, feature = "test-support"))]
pub fn reset_bounded_visibility_declaration_read_count_for_test() {
BOUNDED_VISIBILITY_DECLARATION_READ_COUNT.with(|count| count.set(0));
}
#[cfg(any(test, feature = "test-support"))]
pub fn bounded_visibility_declaration_read_count_for_test() -> usize {
BOUNDED_VISIBILITY_DECLARATION_READ_COUNT.with(Cell::get)
}
#[cfg(any(test, feature = "test-support"))]
pub fn reset_bounded_visibility_dependency_ast_node_count_for_test() {
BOUNDED_VISIBILITY_DEPENDENCY_AST_NODE_COUNT.with(|count| count.set(0));
}
#[cfg(any(test, feature = "test-support"))]
pub fn bounded_visibility_dependency_ast_node_count_for_test() -> usize {
BOUNDED_VISIBILITY_DEPENDENCY_AST_NODE_COUNT.with(Cell::get)
}
#[derive(Default)]
struct OutOfLineOwnerBindingStats {
unseen_owners: usize,
definition_lookups: usize,
admitted: usize,
}
fn extend_with_out_of_line_owner_bindings(
cpp: &dyn CppSource,
visible_by_file: &mut HashMap<ProjectFile, HashSet<CodeUnit>>,
) -> OutOfLineOwnerBindingStats {
let mut stats = OutOfLineOwnerBindingStats::default();
for (file, visible) in visible_by_file.iter_mut() {
let mut unseen_owners: HashSet<String> = visible
.iter()
.filter(|unit| unit.source() == file && (unit.is_function() || unit.is_field()))
.filter_map(brokk_bifrost_core::analyzer::default_parent_fq_name)
.collect();
if unseen_owners.is_empty() {
continue;
}
for unit in visible.iter().filter(|unit| unit.is_class()) {
unseen_owners.remove(&unit.fq_name());
}
stats.unseen_owners += unseen_owners.len();
stats.definition_lookups += unseen_owners.len();
let admitted = unseen_owners
.iter()
.flat_map(|owner| cpp.definitions(owner))
.filter(CodeUnit::is_class)
.collect::<Vec<_>>();
stats.admitted += admitted.len();
visible.extend(admitted);
}
stats
}
pub enum VisibleMemberResolution {
Callable(Vec<CodeUnit>),
NonCallable,
AmbiguousKind,
Missing,
}
#[derive(Clone)]
pub enum EnclosingMemberOwnerResolution {
Owner(CodeUnit),
Ambiguous,
Missing,
}
pub fn resolve_declaring_member_owner(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
receiver_owner: &CodeUnit,
member_name: &str,
) -> EnclosingMemberOwnerResolution {
let Some(hierarchy) = analyzer.type_hierarchy_provider() else {
return EnclosingMemberOwnerResolution::Missing;
};
let Some(receiver_owner) =
visibility.canonical_visible_full_type_unit(analyzer, file, receiver_owner)
else {
return EnclosingMemberOwnerResolution::Ambiguous;
};
let resolve_level = |frontier: &[CodeUnit]| {
let mut member_owners = Vec::new();
for raw_owner in frontier {
let Some(owner) =
visibility.canonical_visible_full_type_unit(analyzer, file, raw_owner)
else {
return EnclosingMemberOwnerResolution::Ambiguous;
};
for member in visibility.visible_members_for_owner_name(file, &owner, member_name) {
if !member.is_field() && !member.is_function() {
continue;
}
let Some(member_owner) = type_owner_of(analyzer, member) else {
return EnclosingMemberOwnerResolution::Ambiguous;
};
if !member_owners
.iter()
.any(|existing| same_visible_symbol(existing, &member_owner))
{
member_owners.push(member_owner);
}
}
}
match member_owners.len() {
0 => EnclosingMemberOwnerResolution::Missing,
1 => EnclosingMemberOwnerResolution::Owner(member_owners.pop().unwrap()),
_ => EnclosingMemberOwnerResolution::Ambiguous,
}
};
let direct = resolve_level(std::slice::from_ref(&receiver_owner));
if !matches!(direct, EnclosingMemberOwnerResolution::Missing) {
return direct;
}
let mut stack = hierarchy
.get_direct_ancestors(&receiver_owner)
.definite()
.to_vec();
let mut propagated_counts: HashMap<CodeUnit, u8> = HashMap::default();
let mut path_matches = Vec::new();
while let Some(raw_owner) = stack.pop() {
let Some(owner) = visibility.canonical_visible_full_type_unit(analyzer, file, &raw_owner)
else {
return EnclosingMemberOwnerResolution::Ambiguous;
};
let propagated = propagated_counts.entry(owner.clone()).or_default();
if *propagated == 2 {
continue;
}
*propagated += 1;
match resolve_level(std::slice::from_ref(&owner)) {
EnclosingMemberOwnerResolution::Owner(owner) => {
path_matches.push(owner);
if path_matches.len() == 2 {
return EnclosingMemberOwnerResolution::Ambiguous;
}
}
EnclosingMemberOwnerResolution::Ambiguous => {
return EnclosingMemberOwnerResolution::Ambiguous;
}
EnclosingMemberOwnerResolution::Missing => {
stack.extend(hierarchy.get_direct_ancestors(&owner).definite().to_vec());
}
}
}
match path_matches.len() {
0 => EnclosingMemberOwnerResolution::Missing,
1 => EnclosingMemberOwnerResolution::Owner(path_matches.pop().unwrap()),
_ => unreachable!("base-path matches are capped at one before returning"),
}
}
pub fn resolve_declaring_callable_owner(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
ordinary: EnclosingMemberOwnerResolution,
member_name: &str,
call_arity: usize,
) -> EnclosingMemberOwnerResolution {
let EnclosingMemberOwnerResolution::Owner(ordinary_owner) = &ordinary else {
return ordinary;
};
if visibility
.visible_members_for_owner_name(file, ordinary_owner, member_name)
.into_iter()
.any(|unit| {
unit.is_function()
&& cpp_callable_arity(analyzer, unit).is_none_or(|known| known.accepts(call_arity))
})
{
return ordinary;
}
let mut pending = match member_using_declaration_bases(
analyzer,
visibility,
file,
ordinary_owner,
member_name,
) {
Ok(bases) => bases,
Err(()) => return EnclosingMemberOwnerResolution::Ambiguous,
};
let mut visited = HashSet::default();
let mut introduced_owners = Vec::new();
while let Some(owner) = pending.pop() {
if !visited.insert(owner.clone()) {
continue;
}
let accepts_arity = visibility
.visible_members_for_owner_name(file, &owner, member_name)
.into_iter()
.any(|unit| {
unit.is_function()
&& cpp_callable_arity(analyzer, unit)
.is_none_or(|known| known.accepts(call_arity))
});
if accepts_arity {
if !introduced_owners
.iter()
.any(|existing| same_visible_symbol(existing, &owner))
{
introduced_owners.push(owner);
}
continue;
}
match member_using_declaration_bases(analyzer, visibility, file, &owner, member_name) {
Ok(bases) => pending.extend(bases),
Err(()) => return EnclosingMemberOwnerResolution::Ambiguous,
}
}
match introduced_owners.as_slice() {
[] => ordinary,
[owner] => EnclosingMemberOwnerResolution::Owner(owner.clone()),
_ => EnclosingMemberOwnerResolution::Ambiguous,
}
}
fn member_using_declaration_bases(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
owner: &CodeUnit,
member_name: &str,
) -> Result<Vec<CodeUnit>, ()> {
let Some(facts) = analyzer.declaration_source_properties(owner) else {
return Err(());
};
let scopes: Vec<_> = facts
.iter()
.flat_map(|fact| &fact.member_usings)
.filter(|using| using.member == member_name)
.map(|using| using.scope.join("::"))
.collect();
if scopes.is_empty() {
return Ok(Vec::new());
}
let Some(hierarchy) = analyzer.type_hierarchy_provider() else {
return Ok(Vec::new());
};
let mut bases = Vec::new();
for raw_ancestor in hierarchy.get_ancestors(owner).definite().to_vec() {
let Some(ancestor) =
visibility.canonical_visible_full_type_unit(analyzer, file, &raw_ancestor)
else {
return Err(());
};
let qualified = cpp_name_for(&ancestor);
if scopes
.iter()
.any(|scope| cpp_qualified_name_has_scope_suffix(&qualified, scope))
&& !bases
.iter()
.any(|existing| same_visible_symbol(existing, &ancestor))
{
bases.push(ancestor);
}
}
Ok(bases)
}
pub fn lexical_component_tiers<'a>(
components: &'a [String],
global: bool,
lexical_scope: &'a [String],
) -> impl Iterator<Item = Vec<String>> + 'a {
let first_prefix_len = if global { 0 } else { lexical_scope.len() };
(0..=first_prefix_len).rev().map(move |prefix_len| {
let mut qualified = Vec::with_capacity(prefix_len + components.len());
qualified.extend_from_slice(&lexical_scope[..prefix_len]);
qualified.extend_from_slice(components);
qualified
})
}
pub fn build_visible_identifier_index(
analyzer: &CppGraphSource<'_>,
visible_by_file: &HashMap<ProjectFile, HashSet<CodeUnit>>,
visible_source_files_by_root: &HashMap<ProjectFile, HashSet<ProjectFile>>,
global_field_internal_linkage: &mut HashMap<CodeUnit, bool>,
) -> HashMap<ProjectFile, HashMap<String, Vec<CodeUnit>>> {
let mut out = HashMap::default();
for (file, visible) in visible_by_file {
let mut by_identifier: HashMap<String, Vec<CodeUnit>> = HashMap::default();
for unit in visible {
if unit.is_field()
&& !visible_source_files_by_root
.get(file)
.is_some_and(|sources| sources.contains(unit.source()))
&& cpp_global_field_has_internal_linkage_cached(
analyzer,
global_field_internal_linkage,
unit,
)
{
continue;
}
by_identifier
.entry(unit.identifier().to_string())
.or_default()
.push(unit.clone());
}
for units in by_identifier.values_mut() {
sort_lookup_units(units);
units.dedup();
}
out.insert(file.clone(), by_identifier);
}
out
}
fn sort_lookup_units(units: &mut [CodeUnit]) {
units.sort_by(|left, right| {
left.fq_name()
.cmp(&right.fq_name())
.then_with(|| left.signature().cmp(&right.signature()))
.then_with(|| left.source().cmp(right.source()))
.then_with(|| left.kind().cmp(&right.kind()))
.then_with(|| {
left.package_segment_count()
.cmp(&right.package_segment_count())
})
.then_with(|| left.is_synthetic().cmp(&right.is_synthetic()))
.then_with(|| stable_fq_name_cmp(left.fq(), right.fq()))
});
}
fn stable_fq_name_cmp(left: &FqName, right: &FqName) -> CmpOrdering {
let interner = segment_interner();
for (&left_id, &right_id) in left.segments().iter().zip(right.segments()) {
let (left_text, left_kind) = interner.resolve(left_id);
let (right_text, right_kind) = interner.resolve(right_id);
let order = left_text
.cmp(right_text)
.then_with(|| segment_kind_order(left_kind).cmp(&segment_kind_order(right_kind)));
if order != CmpOrdering::Equal {
return order;
}
}
left.len().cmp(&right.len())
}
const fn segment_kind_order(kind: SegmentKind) -> u8 {
match kind {
SegmentKind::Path => 0,
SegmentKind::Package => 1,
SegmentKind::Type => 2,
SegmentKind::Companion => 3,
SegmentKind::Nested => 4,
SegmentKind::Member => 5,
SegmentKind::Unknown => 6,
}
}
fn dedup_unit_refs(units: &mut Vec<&CodeUnit>) {
*units = brokk_bifrost_core::analyzer::usages::inverted_edges::dedup_preserving_order(
std::mem::take(units),
);
}
pub fn cpp_reference_fqn_candidates(reference: &str, kind: TargetKind) -> Vec<String> {
let parts = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
reference,
);
cpp_reference_fqn_candidates_from_parts(&parts, kind)
}
fn cpp_reference_fqn_candidates_from_parts(parts: &[String], kind: TargetKind) -> Vec<String> {
if parts.is_empty() {
return Vec::new();
}
let mut candidates = Vec::new();
for package_len in 0..parts.len() {
let package = parts[..package_len].join("::");
let rest = &parts[package_len..];
if rest.is_empty() {
continue;
}
match kind {
TargetKind::Type | TargetKind::Constructor => {
push_cpp_fqn_candidate(&mut candidates, &package, &rest.join("$"));
push_cpp_fqn_candidate(&mut candidates, &package, &rest.join("."));
}
TargetKind::FreeFunction
| TargetKind::Method
| TargetKind::GlobalField
| TargetKind::MemberField
| TargetKind::Macro => {
push_cpp_fqn_candidate(&mut candidates, &package, &rest.join("."));
if rest.len() > 1 {
let owner = rest[..rest.len() - 1].join("$");
let short = format!("{}.{}", owner, rest[rest.len() - 1]);
push_cpp_fqn_candidate(&mut candidates, &package, &short);
}
}
}
}
candidates
}
fn push_cpp_fqn_candidate(out: &mut Vec<String>, package: &str, short: &str) {
let fqn = if package.is_empty() {
short.to_string()
} else {
format!("{package}.{short}")
};
if !out.contains(&fqn) {
out.push(fqn);
}
}
pub fn infer_cpp_initializer_type(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
node: Node<'_>,
) -> Option<CodeUnit> {
infer_cpp_initializer_binding(analyzer, visibility, file, source, node, None)
.and_then(|binding| binding.unit)
}
pub fn infer_cpp_initializer_binding(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
node: Node<'_>,
receiver_resolver: Option<&ReceiverResolver<'_>>,
) -> Option<CppScanBinding> {
match node.kind() {
"new_expression" => {
let type_node = cpp_new_expression_type_node(node)?;
let type_text = node_text(type_node, source);
let name = normalize_cpp_type_name(type_text);
Some(CppScanBinding::from_type_name(
name.clone(),
visibility.resolve_type(file, &name),
1,
))
}
"call_expression" => node.child_by_field_name("function").and_then(|function| {
if function.kind() == "field_expression" {
let arity = visibility.call_arity_evidence(file, node, source).exact()?;
return resolve_field_method_call_return_binding(
analyzer,
visibility,
file,
source,
function,
arity,
receiver_resolver,
);
}
let function_text = node_text(function, source);
let direct_type_binding = visibility
.resolve_type(file, function_text)
.map(|unit| CppScanBinding::from_unit(unit, 0));
if function.kind() == "template_function" && direct_type_binding.is_some() {
let lexical_namespace = enclosing_namespace_context(node, source);
let arity = visibility.call_arity_evidence(file, node, source).exact();
if let Some(arity) = arity
&& let Some(binding) = visibility.resolve_call_return_binding(
analyzer,
file,
function_text,
arity,
lexical_namespace.as_deref(),
direct_type_binding
.as_ref()
.and_then(|binding| binding.unit.as_ref()),
)
{
return Some(binding);
}
let (has_callable, callable_binding) = visibility
.resolve_call_return_binding_without_arity(
analyzer,
file,
function_text,
lexical_namespace.as_deref(),
direct_type_binding
.as_ref()
.and_then(|binding| binding.unit.as_ref()),
);
if let Some(binding) = callable_binding {
return Some(binding);
}
if has_callable {
return None;
}
return direct_type_binding;
}
let arity = visibility.call_arity_evidence(file, node, source).exact();
if let Some(arity) = arity {
let direct_type_binding_for_call = direct_type_binding.clone();
if let Some(binding) = resolve_static_method_call_return_binding(
analyzer, visibility, file, source, function, arity,
)
.or_else(|| {
visibility.resolve_call_return_binding(
analyzer,
file,
function_text,
arity,
enclosing_namespace_context(node, source).as_deref(),
direct_type_binding_for_call
.as_ref()
.and_then(|binding| binding.unit.as_ref()),
)
}) {
return Some(binding);
}
}
direct_type_binding
}),
_ => None,
}
}
pub fn cpp_new_expression_type_node(expression: Node<'_>) -> Option<Node<'_>> {
debug_assert_eq!(expression.kind(), "new_expression");
if let Some(type_node) = expression.child_by_field_name("type") {
return Some(type_node);
}
let mut pending = Vec::new();
let mut cursor = expression.walk();
pending.extend(
expression
.named_children(&mut cursor)
.filter(|child| {
!matches!(
child.kind(),
"argument_list" | "initializer_list" | "new_declarator"
)
})
.collect::<Vec<_>>()
.into_iter()
.rev(),
);
while let Some(node) = pending.pop() {
if is_cpp_type_specifier_node(node) {
return Some(node);
}
if node.kind() == "ERROR" {
let mut cursor = node.walk();
pending.extend(
node.named_children(&mut cursor)
.collect::<Vec<_>>()
.into_iter()
.rev(),
);
}
}
None
}
fn is_cpp_type_specifier_node(node: Node<'_>) -> bool {
matches!(
node.kind(),
"class_specifier"
| "decltype"
| "dependent_type"
| "enum_specifier"
| "placeholder_type_specifier"
| "primitive_type"
| "qualified_identifier"
| "sized_type_specifier"
| "struct_specifier"
| "template_type"
| "type_identifier"
| "union_specifier"
)
}
fn resolve_static_method_call_return_binding(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
function: Node<'_>,
arity: usize,
) -> Option<CppScanBinding> {
if function.kind() != "qualified_identifier" {
return None;
}
let qualified = normalize_cpp_reference_text(node_text(function, source));
let parts = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
&qualified,
);
let (owner_text, member_name) = match parts.split_last() {
Some((member, owner_parts)) if !owner_parts.is_empty() => {
(owner_parts.join("::"), member.clone())
}
_ => {
let scope = function.child_by_field_name("scope")?;
let name = function.child_by_field_name("name")?;
(
node_text(scope, source).to_string(),
node_text(name, source).to_string(),
)
}
};
let owner = visibility.resolve_type(file, &owner_text)?;
let candidates =
callable_members_accepting_arity(analyzer, visibility, file, &owner, &member_name, arity);
unanimous_return_binding(analyzer, visibility, file, &candidates)
}
fn resolve_field_method_call_return_binding(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
function: Node<'_>,
arity: usize,
receiver_resolver: Option<&ReceiverResolver<'_>>,
) -> Option<CppScanBinding> {
debug_assert_eq!(
function.kind(),
"field_expression",
"the member-call return binding answers only for a field-expression callee"
);
let receiver_resolver = receiver_resolver?;
let field = function.child_by_field_name("field")?;
let member_name = node_text(function_terminal_node(field), source);
let receiver = function
.child_by_field_name("argument")
.or_else(|| function.named_child(0))?;
let owners = receiver_resolver(receiver, source);
let mut candidates = Vec::new();
for owner in owners {
let declaring_owner =
match resolve_declaring_member_owner(analyzer, visibility, file, &owner, member_name) {
EnclosingMemberOwnerResolution::Owner(owner) => owner,
EnclosingMemberOwnerResolution::Missing => continue,
EnclosingMemberOwnerResolution::Ambiguous => return None,
};
candidates.extend(callable_members_accepting_arity(
analyzer,
visibility,
file,
&declaring_owner,
member_name,
arity,
));
}
unanimous_return_binding(analyzer, visibility, file, &candidates)
}
fn callable_members_accepting_arity(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
owner: &CodeUnit,
member_name: &str,
arity: usize,
) -> Vec<CodeUnit> {
visibility
.visible_members_for_owner_name(file, owner, member_name)
.into_iter()
.filter(|unit| {
unit.is_function()
&& cpp_callable_arity(analyzer, unit).is_none_or(|known| known.accepts(arity))
})
.cloned()
.collect()
}
fn unanimous_return_binding(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
candidates: &[CodeUnit],
) -> Option<CppScanBinding> {
let mut resolved_return: Option<CppScanBinding> = None;
for function in candidates {
let metadata = analyzer.signature_metadata(function);
let return_types = if metadata.is_empty() {
vec![cpp_function_return_type_text(analyzer, function)?]
} else {
metadata
.iter()
.map(|metadata| metadata.return_type_text().map(str::to_string))
.collect::<Option<Vec<_>>>()?
};
for return_text in return_types {
let indirection = crate::call_match::cpp_type_text_pointer_depth(&return_text);
let name = normalize_cpp_type_name(&return_text);
let binding = CppScanBinding::from_type_name(
name.clone(),
visibility
.resolve_unique_canonical_type_for_declaration(analyzer, file, function, &name),
indirection,
);
if let Some(existing) = resolved_return.as_ref()
&& (existing.indirection != binding.indirection
|| match (&existing.unit, &binding.unit) {
(Some(left), Some(right)) => !same_visible_symbol(left, right),
(None, None) => existing.type_name != binding.type_name,
(Some(_), None) | (None, Some(_)) => true,
})
{
return None;
}
resolved_return = Some(binding);
}
}
resolved_return
}
fn aliases_from_canonical_source(
cpp: &dyn CppSource,
token: QueryToken<'_>,
file: &ProjectFile,
) -> Option<Vec<CppAlias>> {
let source = cpp.declaration_source_facts(token, file)?;
Some(
source
.facts
.declarations
.iter()
.filter_map(|fact| {
let alias = fact.file_scope_alias.as_ref()?;
Some(CppAlias {
name: alias.name.clone(),
target: alias.target.clone(),
namespace: alias.namespace.clone(),
})
})
.collect(),
)
}
pub(crate) fn alias_has_visible_file_scope(node: Node<'_>) -> bool {
let mut current = node.parent();
while let Some(parent) = current {
match parent.kind() {
"translation_unit"
| "namespace_definition"
| "declaration_list"
| "linkage_specification" => current = parent.parent(),
"template_declaration" => current = parent.parent(),
_ => return false,
}
}
true
}
pub fn collect_include_closure(
analyzer: &CppGraphSource<'_>,
include_targets: &IncludeTargetIndex,
file: &ProjectFile,
out: &mut HashSet<ProjectFile>,
cancellation: Option<&CancellationToken>,
) {
let mut stack = vec![file.clone()];
while let Some(file) = stack.pop() {
if cancellation.is_some_and(CancellationToken::is_cancelled) {
break;
}
if !out.insert(file.clone()) {
continue;
}
for include in analyzer.canonical_include_paths(&file).unwrap_or_default() {
for target in resolve_include_targets_with_index(&file, &include, include_targets) {
stack.push(target);
}
}
}
}
fn parse_macro_parameter_list<T>(
replacement: &str,
read: impl FnOnce(Node<'_>, &str) -> Option<T>,
) -> Option<T> {
let parsed = parse_sentinel(
"void __bifrost_macro_parameters(",
SentinelInput::replacement(replacement),
");",
);
let (source, tree) = (parsed.source, parsed.tree);
let root = tree.root_node();
if root.has_error() {
return None;
}
let declaration = single_named_child_matching(root, |child| !child.is_extra())?;
let declarator = declaration.child_by_field_name("declarator")?;
let parameters = declarator.child_by_field_name("parameters")?;
read(parameters, &source)
}
fn parse_macro_parameter_list_arity(replacement: &str) -> Option<CallableArity> {
parse_macro_parameter_list(replacement, crate::declarations::cpp_callable_arity)
}
pub fn cpp_macro_parameter_types(replacement: &str) -> Option<Vec<String>> {
parse_macro_parameter_list(
replacement,
crate::declarations::cpp_callable_parameter_types,
)
}
pub fn cpp_callable_arity(analyzer: &CppGraphSource<'_>, unit: &CodeUnit) -> Option<CallableArity> {
brokk_bifrost_core::analyzer::model::first_callable_arity(analyzer.signature_metadata(unit))
}
pub fn cpp_callable_parameter_types(
analyzer: &CppGraphSource<'_>,
unit: &CodeUnit,
) -> Option<Vec<String>> {
brokk_bifrost_core::analyzer::model::first_callable_parameter_types(
analyzer.signature_metadata(unit),
)
}
fn merge_compatible_callable_arities(
left: CallableArity,
right: CallableArity,
) -> Option<CallableArity> {
let total = left.total();
let left_repeated = left.accepts(total.saturating_add(1));
let right_repeated = right.accepts(right.total().saturating_add(1));
if total != right.total() || left_repeated != right_repeated {
return None;
}
let required = (0..=total).find(|arity| left.accepts(*arity) || right.accepts(*arity))?;
Some(CallableArity::new(required, total, left_repeated))
}
fn collect_macro_events(
cpp: &dyn CppSource,
token: QueryToken<'_>,
file: &ProjectFile,
) -> Vec<MacroEvent> {
let Some(prepared) = cpp.prepared_syntax(token, file) else {
return Vec::new();
};
let source = prepared.source();
let mut events = Vec::new();
let root = prepared.tree().root_node();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
match node.kind() {
"preproc_def" | "preproc_function_def" => {
let Some(name) = node.child_by_field_name("name") else {
continue;
};
let name = node_text(name, source).to_string();
events.push(MacroEvent::Define {
name,
binding: MacroBinding {
source: file.clone(),
declaration_byte: node.start_byte(),
definition: VisibilityIndex::decode_macro_definition(node, source),
exact: true,
},
byte: node.start_byte(),
conditionals: owning_preprocessor_conditionals(root, node, source),
});
continue;
}
"preproc_include" => {
let Some(path) = node.child_by_field_name("path") else {
events.push(MacroEvent::Include {
targets: Vec::new(),
byte: node.start_byte(),
conditionals: owning_preprocessor_conditionals(root, node, source),
});
continue;
};
let include = crate::imports::include_path_literal_value(path, source);
let targets = include.map_or_else(Vec::new, |include| {
resolve_include_targets_with_index(file, include, &cpp.include_target_index())
});
if targets.is_empty()
&& include.is_some_and(|include| {
!cpp.include_target_index().names_indexed_file(include)
})
{
continue;
}
events.push(MacroEvent::Include {
targets,
byte: node.start_byte(),
conditionals: owning_preprocessor_conditionals(root, node, source),
});
continue;
}
"preproc_call" => {
let Some(directive) = node.child_by_field_name("directive") else {
continue;
};
if node_text(directive, source) != "#undef" {
continue;
}
let name = node
.child_by_field_name("argument")
.and_then(|argument| parse_preproc_identifier(node_text(argument, source)));
if let Some(name) = name {
events.push(MacroEvent::Undef {
name,
byte: node.start_byte(),
conditionals: owning_preprocessor_conditionals(root, node, source),
});
} else {
events.push(MacroEvent::Invalidate {
byte: node.start_byte(),
});
}
continue;
}
_ => {}
}
push_named_children_reversed(node, &mut stack);
}
events.sort_by_key(MacroEvent::byte);
events
}
fn find_include_activation<'guards>(
cpp: &dyn CppSource,
token: QueryToken<'_>,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
donor_source: &ProjectFile,
effective_guards: &dyn Fn() -> Option<&'guards HashSet<PreprocessorGuard>>,
) -> Option<usize> {
let include_targets = cpp.include_target_index();
let mut direct_includes = Vec::new();
let mut nodes = vec![prepared.tree().root_node()];
while let Some(node) = nodes.pop() {
if node.kind() == "preproc_include" {
let Some(required_guards) =
include_directive_guard_requirements(node, prepared.source(), effective_guards)
else {
continue;
};
if required_guards.all.is_empty() && required_guards.partial.is_empty() {
for (include, _) in include_path_from_node(node, prepared.source()).into_iter() {
if let Some(target) = unique_include_target(resolve_include_targets_with_index(
file,
include,
&include_targets,
)) {
direct_includes.push((node.end_byte(), target));
}
}
}
continue;
}
push_named_children_reversed(node, &mut nodes);
}
direct_includes.sort_by_key(|(activation, _)| *activation);
let mut known_missing = HashSet::default();
direct_includes
.into_iter()
.find(|(_, direct)| {
unconditional_include_reaches(
cpp,
token,
direct,
donor_source,
file,
&mut known_missing,
effective_guards,
)
})
.map(|(activation, _)| activation)
}
fn find_conditional_include_projection_index<'guards>(
cpp: &dyn CppSource,
token: QueryToken<'_>,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
effective_guards: &dyn Fn() -> Option<&'guards HashSet<PreprocessorGuard>>,
on_state: &dyn Fn(),
) -> ConditionalIncludeProjectionIndex {
let include_targets = cpp.include_target_index();
let mut projections_by_source: HashMap<ProjectFile, Vec<ConditionalIncludeProjection>> =
HashMap::default();
let mut pending = Vec::new();
let mut nodes = vec![prepared.tree().root_node()];
while let Some(node) = nodes.pop() {
if node.kind() == "preproc_include" {
let Some(required_guards) =
include_directive_guard_requirements(node, prepared.source(), effective_guards)
else {
continue;
};
for (include, _) in include_path_from_node(node, prepared.source()).into_iter() {
let Some(target) = unique_include_target(resolve_include_targets_with_index(
file,
include,
&include_targets,
)) else {
continue;
};
pending.push((target, node.end_byte(), required_guards.clone()));
}
continue;
}
push_named_children_reversed(node, &mut nodes);
}
let mut expanded: HashMap<(ProjectFile, usize), Vec<PreprocessorGuardEnvironment>> =
HashMap::default();
while let Some((current_file, activation_byte, path)) = pending.pop() {
let guard_sets = expanded
.entry((current_file.clone(), activation_byte))
.or_default();
if guard_sets.iter().any(|existing| existing.is_subset(&path)) {
continue;
}
let (evicted, kept): (Vec<_>, Vec<_>) = guard_sets
.drain(..)
.partition(|existing| path.is_subset(existing));
*guard_sets = kept;
guard_sets.push(path.clone());
if !evicted.is_empty()
&& let Some(projections) = projections_by_source.get_mut(¤t_file)
{
projections.retain(|projection| {
projection.activation_byte != activation_byte
|| !evicted.iter().any(|existing| {
existing.all == projection.required_guards
&& existing.partial == projection.partial_guards
})
});
}
on_state();
projections_by_source
.entry(current_file.clone())
.or_default()
.push(ConditionalIncludeProjection {
activation_byte,
required_guards: path.all.clone(),
partial_guards: path.partial.clone(),
});
let Some(current_prepared) = cpp.prepared_syntax(token, ¤t_file) else {
continue;
};
let mut nodes = vec![current_prepared.tree().root_node()];
while let Some(node) = nodes.pop() {
if node.kind() == "preproc_include" {
let Some(include_guards) = include_directive_guard_requirements(
node,
current_prepared.source(),
effective_guards,
) else {
continue;
};
let Some(reached) = path.merged(&include_guards) else {
continue;
};
for (include, _) in
include_path_from_node(node, current_prepared.source()).into_iter()
{
let Some(target) = unique_include_target(resolve_include_targets_with_index(
¤t_file,
include,
&include_targets,
)) else {
continue;
};
pending.push((target, activation_byte, reached.clone()));
}
continue;
}
push_named_children_reversed(node, &mut nodes);
}
}
projections_by_source
.into_iter()
.map(|(source, mut projections)| {
projections.sort_by_key(|projection| projection.activation_byte);
(source, Arc::from(projections))
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn find_conditional_include_projection_for_source<'guards>(
cpp: &dyn CppSource,
token: QueryToken<'_>,
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
donor_source: &ProjectFile,
admission: IncludePathAdmission,
reference_guards: Option<&HashSet<PreprocessorGuard>>,
reference_byte: usize,
effective_guards: &dyn Fn() -> Option<&'guards HashSet<PreprocessorGuard>>,
on_state: &dyn Fn(),
) -> bool {
let Some(reference_guards) = reference_guards else {
return false;
};
let include_targets = cpp.include_target_index();
let mut pending = Vec::new();
let mut nodes = vec![prepared.tree().root_node()];
while let Some(node) = nodes.pop() {
if node.kind() == "preproc_include" {
let Some(required_guards) =
include_directive_guard_requirements(node, prepared.source(), effective_guards)
else {
continue;
};
if node.end_byte() > reference_byte
|| !admission.admits(
&required_guards.all,
&required_guards.partial,
Some(reference_guards),
)
{
continue;
}
for (include, _) in include_path_from_node(node, prepared.source()).into_iter() {
let Some(target) = unique_include_target(resolve_include_targets_with_index(
file,
include,
&include_targets,
)) else {
continue;
};
if &target == donor_source {
return true;
}
pending.push((target, required_guards.clone()));
}
continue;
}
push_named_children_reversed(node, &mut nodes);
}
let mut expanded: HashMap<ProjectFile, Vec<HashSet<PreprocessorGuard>>> = HashMap::default();
while let Some((current_file, path)) = pending.pop() {
let guard_sets = expanded.entry(current_file.clone()).or_default();
if guard_sets.contains(&path.all) {
continue;
}
guard_sets.push(path.all.clone());
on_state();
let Some(current_prepared) = cpp.prepared_syntax(token, ¤t_file) else {
continue;
};
let mut nodes = vec![current_prepared.tree().root_node()];
while let Some(node) = nodes.pop() {
if node.kind() == "preproc_include" {
let Some(include_guards) = include_directive_guard_requirements(
node,
current_prepared.source(),
effective_guards,
) else {
continue;
};
let Some(reached) = path.merged(&include_guards) else {
continue;
};
if !admission.admits(&reached.all, &reached.partial, Some(reference_guards)) {
continue;
}
for (include, _) in
include_path_from_node(node, current_prepared.source()).into_iter()
{
let Some(target) = unique_include_target(resolve_include_targets_with_index(
¤t_file,
include,
&include_targets,
)) else {
continue;
};
if &target == donor_source {
return true;
}
pending.push((target, reached.clone()));
}
continue;
}
push_named_children_reversed(node, &mut nodes);
}
}
false
}
pub fn cpp_include_closure_reaches(
cpp: &dyn CppSource,
token: QueryToken<'_>,
translation_unit: &ProjectFile,
header: &ProjectFile,
) -> bool {
let language_guard_stable = OnceCell::new();
let effective = OnceCell::new();
let proven = compile_proven_guard_facts(cpp, translation_unit);
unconditional_include_reaches(
cpp,
token,
translation_unit,
header,
translation_unit,
&mut HashSet::default(),
&|| {
effective
.get_or_init(|| {
effective_reference_guards(
cpp,
translation_unit,
Some(&HashSet::default()),
&proven,
&|| {
*language_guard_stable.get_or_init(|| {
include_language_guard_is_stable_with(
cpp,
token,
translation_unit,
&mut |file: &ProjectFile| {
collect_macro_events(cpp, token, file).into_boxed_slice()
},
&mut |_: &ProjectFile, prepared: &PreparedSyntaxTree| {
collect_structured_include_facts(prepared)
},
)
})
},
)
})
.as_ref()
},
)
}
fn unconditional_include_reaches<'guards>(
cpp: &dyn CppSource,
token: QueryToken<'_>,
first: &ProjectFile,
donor_source: &ProjectFile,
reference_file: &ProjectFile,
known_missing: &mut HashSet<ProjectFile>,
effective_guards: &dyn Fn() -> Option<&'guards HashSet<PreprocessorGuard>>,
) -> bool {
if first == donor_source {
return true;
}
if known_missing.contains(first) {
return false;
}
if let Some(reaches) =
cpp.cached_unconditional_include_reachability(first, donor_source, reference_file)
{
return reaches;
}
let mut visited = HashSet::default();
let mut files = vec![first.clone()];
let include_targets = cpp.include_target_index();
while let Some(file) = files.pop() {
if file == *donor_source {
cpp.cache_unconditional_include_reachability(first, donor_source, reference_file, true);
return true;
}
if known_missing.contains(&file) || !visited.insert(file.clone()) {
continue;
}
let Some(prepared) = cpp.prepared_syntax(token, &file) else {
continue;
};
let mut cursor = prepared.tree().walk();
let mut nodes = vec![prepared.tree().root_node()];
while let Some(node) = nodes.pop() {
if node.kind() == "preproc_include" {
let Some(required_guards) =
include_directive_guard_requirements(node, prepared.source(), effective_guards)
else {
continue;
};
if required_guards.all.is_empty() && required_guards.partial.is_empty() {
for (include, _) in include_path_from_node(node, prepared.source()).into_iter()
{
if let Some(target) = unique_include_target(
resolve_include_targets_with_index(&file, include, &include_targets),
) {
files.push(target);
}
}
}
continue;
}
let first_pushed = nodes.len();
nodes.extend(node.named_children(&mut cursor));
nodes[first_pushed..].reverse();
}
}
known_missing.extend(visited);
cpp.cache_unconditional_include_reachability(first, donor_source, reference_file, false);
false
}
fn effective_reference_guards(
cpp: &dyn CppSource,
file: &ProjectFile,
reference_guards: Option<&HashSet<PreprocessorGuard>>,
proven: &HashSet<PreprocessorGuard>,
language_guard_is_stable: &dyn Fn() -> bool,
) -> Option<HashSet<PreprocessorGuard>> {
let mut effective = reference_guards.cloned()?;
effective.extend(proven.iter().cloned());
if language_guard_is_stable() {
effective.insert(if reference_uses_c_semantics(cpp, file) {
PreprocessorGuard::Undefined("__cplusplus".to_string())
} else {
PreprocessorGuard::Defined("__cplusplus".to_string())
});
}
Some(effective)
}
fn include_language_guard_is_stable_with<MacroEvents, IncludeFacts>(
cpp: &dyn CppSource,
token: QueryToken<'_>,
file: &ProjectFile,
mut macro_events: MacroEvents,
mut include_facts: IncludeFacts,
) -> bool
where
MacroEvents: FnMut(&ProjectFile) -> Box<[MacroEvent]>,
IncludeFacts: FnMut(&ProjectFile, &PreparedSyntaxTree) -> Arc<[StructuredIncludeFact]>,
{
if !crate::imports::is_cpp_translation_unit(file) {
return false;
}
if !cpp.compile_contexts_for(file).is_empty()
|| cpp
.reaching_translation_units(file)
.iter()
.any(|tu| !cpp.compile_contexts_for(tu).is_empty())
{
return false;
}
let mut pending = vec![file.clone()];
let mut visited = HashSet::default();
while let Some(file) = pending.pop() {
if !visited.insert(file.clone()) {
continue;
}
let Some(prepared) = cpp.prepared_syntax(token, &file) else {
return false;
};
if macro_events(&file).iter().any(|event| match event {
MacroEvent::Define { name, .. } | MacroEvent::Undef { name, .. } => {
name == "__cplusplus"
}
MacroEvent::Invalidate { .. } => true,
MacroEvent::Include { targets, .. } => targets.is_empty(),
}) {
return false;
}
for fact in include_facts(&file, prepared.as_ref()).iter() {
let targets =
resolve_include_targets_with_index(&file, &fact.path, &cpp.include_target_index());
if targets.is_empty() {
return false;
}
pending.extend(targets);
}
}
true
}
fn declaration_guard_requirements(
analyzer: &CppGraphSource<'_>,
cpp: &dyn CppSource,
candidate: &CodeUnit,
) -> Vec<(usize, HashSet<PreprocessorGuard>)> {
if cpp
.prepared_syntax(analyzer.token, candidate.source())
.is_none()
{
return Vec::new();
}
analyzer
.declaration_source_occurrences(candidate)
.into_iter()
.flatten()
.filter_map(|occurrence| {
let guards = cpp_guard_set_to_runtime(occurrence.fact.guard_requirements.as_ref()?)?;
Some((occurrence.range.start_byte, guards))
})
.collect()
}
struct DeclarationGuardSite {
byte: usize,
guards: HashSet<PreprocessorGuard>,
completed_family_before_reference: bool,
}
fn declaration_guard_sites(
analyzer: &CppGraphSource<'_>,
cpp: &dyn CppSource,
candidate: &CodeUnit,
reference_byte: usize,
) -> Vec<DeclarationGuardSite> {
let Some(prepared) = cpp.prepared_syntax(analyzer.token, candidate.source()) else {
return Vec::new();
};
let root = prepared.tree().root_node();
analyzer
.declaration_source_occurrences(candidate)
.into_iter()
.flatten()
.filter_map(|occurrence| {
let guards = cpp_guard_set_to_runtime(occurrence.fact.guard_requirements.as_ref()?)?;
Some(DeclarationGuardSite {
byte: occurrence.range.start_byte,
guards,
completed_family_before_reference: {
root.descendant_for_byte_range(
occurrence.range.start_byte,
occurrence.range.end_byte,
)
.is_some_and(|node| {
declaration_follows_completed_family_before_reference(
node,
prepared.source(),
reference_byte,
)
})
},
})
})
.collect()
}
fn declaration_follows_completed_family_before_reference(
node: Node<'_>,
source: &str,
reference_byte: usize,
) -> bool {
callable_guard_completion_byte(node, source).is_some_and(|end| end > 0 && end <= reference_byte)
}
pub(crate) fn callable_guard_completion_byte(node: Node<'_>, source: &str) -> Option<usize> {
let mut cache = PreprocessorContextCache::default();
callable_guard_completion_byte_with_cache(node, source, &mut cache)
}
pub(crate) fn callable_guard_completion_byte_with_cache(
node: Node<'_>,
source: &str,
cache: &mut PreprocessorContextCache,
) -> Option<usize> {
let mut completion_byte = 0;
let mut ancestor = node.parent();
while let Some(conditional) = ancestor {
if matches!(
conditional.kind(),
"preproc_if" | "preproc_ifdef" | "preproc_elif"
) && !is_file_covering_include_guard(conditional, source)
&& !is_split_cpp_language_linkage_wrapper(conditional, node, source)
&& preprocessor_conditional_contains_descendant_with_cache(conditional, node, cache)
{
let family = preprocessor_conditional_family_root(conditional);
if !preprocessor_conditional_family_has_terminal_else(family, cache) {
return None;
}
completion_byte = completion_byte.max(family.end_byte());
}
ancestor = conditional.parent();
}
Some(completion_byte)
}
fn first_declaration_byte(analyzer: &CppGraphSource<'_>, candidate: &CodeUnit) -> Option<usize> {
analyzer
.ranges(candidate)
.into_iter()
.map(|range| range.start_byte)
.min()
}
fn compile_proven_guard_facts(
cpp: &dyn CppSource,
file: &ProjectFile,
) -> HashSet<PreprocessorGuard> {
if let Some(facts) = context_guard_facts(&cpp.compile_contexts_for(file), file) {
return facts;
}
let mut translation_units = cpp.reaching_translation_units(file).into_iter();
let Some(mut facts) = translation_units.next().and_then(|translation_unit| {
context_guard_facts(
&cpp.compile_contexts_for(&translation_unit),
&translation_unit,
)
}) else {
return HashSet::default();
};
for translation_unit in translation_units {
let Some(reached) = context_guard_facts(
&cpp.compile_contexts_for(&translation_unit),
&translation_unit,
) else {
facts.clear();
break;
};
facts.retain(|fact| reached.contains(fact));
if facts.is_empty() {
break;
}
}
facts
}
fn context_guard_facts(
contexts: &[CppCompileContext],
file: &ProjectFile,
) -> Option<HashSet<PreprocessorGuard>> {
let (first, rest) = contexts.split_first()?;
let mut facts = first
.defined_macros
.iter()
.filter(|name| {
rest.iter()
.all(|context| context.defined_macros.contains(*name))
})
.cloned()
.map(PreprocessorGuard::Defined)
.collect::<HashSet<_>>();
let language = first.tu_language(file.rel_path());
if contexts
.iter()
.all(|context| !context.cplusplus_macro_configured)
&& rest
.iter()
.all(|context| context.tu_language(file.rel_path()) == language)
{
facts.insert(match language {
CompiledLanguage::C => PreprocessorGuard::Undefined("__cplusplus".to_string()),
CompiledLanguage::Cpp => PreprocessorGuard::Defined("__cplusplus".to_string()),
});
}
Some(facts)
}
pub fn guard_requirements_hold_at_reference(
required: &HashSet<PreprocessorGuard>,
reference: Option<&HashSet<PreprocessorGuard>>,
) -> bool {
reference.is_some_and(|active| {
required
.iter()
.all(|guard| preprocessor_guard_holds_at_reference(guard, active))
})
}
fn preprocessor_guard_holds_at_reference(
required: &PreprocessorGuard,
active: &HashSet<PreprocessorGuard>,
) -> bool {
if active.contains(required) {
return true;
}
let active_expression =
BooleanGuardExpression::all(active.iter().map(PreprocessorGuard::as_boolean_expression));
active_expression.implies(&required.as_boolean_expression())
}
fn guards_compatible_at_reference(
declaration: &HashSet<PreprocessorGuard>,
reference: Option<&HashSet<PreprocessorGuard>>,
) -> bool {
reference.is_some_and(|active| merge_preprocessor_guards(declaration, active).is_some())
}
pub(crate) fn preprocessor_conditional_family_node(node: Node<'_>) -> Option<Node<'_>> {
let mut ancestor = Some(node);
while let Some(current) = ancestor {
if is_cpp_preprocessor_conditional(current)
&& preprocessor_conditional_contains_descendant(current, node)
{
return Some(preprocessor_conditional_family_root(current));
}
ancestor = current.parent();
}
None
}
pub(crate) fn preprocessor_conditional_family_for_declaration_with_cache<'tree>(
node: Node<'tree>,
cache: &mut PreprocessorContextCache,
) -> Option<Node<'tree>> {
let mut ancestor = node.parent();
while let Some(current) = ancestor {
if is_cpp_preprocessor_conditional(current)
&& preprocessor_conditional_contains_descendant_with_cache(current, node, cache)
{
let family = preprocessor_conditional_family_root(current);
if preprocessor_conditional_family_has_terminal_else(family, cache) {
return Some(family);
}
}
ancestor = current.parent();
}
None
}
fn preprocessor_conditional_family_root(mut conditional: Node<'_>) -> Node<'_> {
while let Some(parent) = conditional.parent() {
let is_alternative = parent
.child_by_field_name("alternative")
.is_some_and(|alternative| {
alternative.start_byte() == conditional.start_byte()
&& alternative.end_byte() == conditional.end_byte()
});
if !is_alternative {
break;
}
conditional = parent;
}
conditional
}
fn preprocessor_conditional_family_has_terminal_else(
mut conditional: Node<'_>,
cache: &mut PreprocessorContextCache,
) -> bool {
loop {
let Some(alternative) = conditional.child_by_field_name("alternative") else {
return cache
.displaced_branch(conditional)
.is_some_and(|directive| directive.is_else);
};
match alternative.kind() {
"preproc_else" => return true,
"preproc_elif" => conditional = alternative,
_ => return false,
}
}
}
fn include_directive_guard_requirements<'guards>(
node: Node<'_>,
source: &str,
effective_guards: &dyn Fn() -> Option<&'guards HashSet<PreprocessorGuard>>,
) -> Option<PreprocessorGuardEnvironment> {
let mut required = preprocessor_guard_environment_by_family(node, source)?;
if !required.all.is_empty() {
let effective = effective_guards();
if !guards_compatible_at_reference(&required.all, effective) {
return None;
}
for guards in [&mut required.all, &mut required.partial] {
guards.retain(|guard| {
!effective
.is_some_and(|active| preprocessor_guard_holds_at_reference(guard, active))
});
}
}
Some(required)
}
pub fn preprocessor_guard_environment(
node: Node<'_>,
source: &str,
) -> Option<HashSet<PreprocessorGuard>> {
let mut cache = PreprocessorContextCache::default();
preprocessor_guard_environment_with_cache(node, source, &mut cache)
}
pub(crate) fn preprocessor_guard_environment_with_cache(
node: Node<'_>,
source: &str,
cache: &mut PreprocessorContextCache,
) -> Option<HashSet<PreprocessorGuard>> {
preprocessor_guard_environment_by_family_with_cache(node, source, cache)
.map(|environment| environment.all)
}
#[derive(Clone)]
struct PreprocessorGuardEnvironment {
all: HashSet<PreprocessorGuard>,
partial: HashSet<PreprocessorGuard>,
}
impl PreprocessorGuardEnvironment {
fn is_subset(&self, other: &Self) -> bool {
self.all.is_subset(&other.all) && self.partial.is_subset(&other.partial)
}
fn merged(&self, other: &Self) -> Option<Self> {
Some(Self {
all: merge_preprocessor_guards(&self.all, &other.all)?,
partial: self.partial.union(&other.partial).cloned().collect(),
})
}
}
fn preprocessor_guard_environment_by_family(
node: Node<'_>,
source: &str,
) -> Option<PreprocessorGuardEnvironment> {
let mut cache = PreprocessorContextCache::default();
preprocessor_guard_environment_by_family_with_cache(node, source, &mut cache)
}
fn preprocessor_guard_environment_by_family_with_cache(
node: Node<'_>,
source: &str,
cache: &mut PreprocessorContextCache,
) -> Option<PreprocessorGuardEnvironment> {
let mut all = HashSet::default();
let mut partial = HashSet::default();
let mut ancestor = node.parent();
while let Some(conditional) = ancestor {
if matches!(
conditional.kind(),
"preproc_if" | "preproc_ifdef" | "preproc_elif"
) && !is_file_covering_include_guard(conditional, source)
&& !is_split_cpp_language_linkage_wrapper(conditional, node, source)
&& preprocessor_conditional_contains_descendant_with_cache(conditional, node, cache)
{
let guard = preprocessor_guard_for_descendant(conditional, node, source, cache)?;
match guard {
PreprocessorGuard::Constant(true) => {
ancestor = conditional.parent();
continue;
}
PreprocessorGuard::Constant(false) => return None,
_ => {}
}
if all.contains(&guard.negated()) {
return None;
}
if !preprocessor_conditional_family_has_terminal_else(
preprocessor_conditional_family_root(conditional),
cache,
) {
partial.insert(guard.clone());
}
all.insert(guard);
}
ancestor = conditional.parent();
}
if let Some(guard) = fragmented_statement_preprocessor_guard(node, source) {
match guard {
PreprocessorGuard::Constant(true) => {}
PreprocessorGuard::Constant(false) => return None,
_ => {
if all.contains(&guard.negated()) {
return None;
}
partial.insert(guard.clone());
all.insert(guard);
}
}
}
Some(PreprocessorGuardEnvironment { all, partial })
}
pub(crate) fn callable_preprocessor_guard_requirements_with_cache(
node: Node<'_>,
source: &str,
cache: &mut PreprocessorContextCache,
) -> Option<HashSet<PreprocessorGuard>> {
let mut guards = HashSet::default();
for guard in callable_preprocessor_ancestor_guards_with_cache(node, source, cache)? {
match guard {
PreprocessorGuard::Constant(true) => {}
PreprocessorGuard::Constant(false) => return None,
_ => {
if guards.contains(&guard.negated()) {
return None;
}
guards.insert(guard);
}
}
}
Some(guards)
}
fn callable_preprocessor_ancestor_guards(
node: Node<'_>,
source: &str,
) -> Option<Vec<PreprocessorGuard>> {
let mut cache = PreprocessorContextCache::default();
callable_preprocessor_ancestor_guards_with_cache(node, source, &mut cache)
}
fn callable_preprocessor_ancestor_guards_with_cache(
node: Node<'_>,
source: &str,
cache: &mut PreprocessorContextCache,
) -> Option<Vec<PreprocessorGuard>> {
let mut guards = Vec::new();
let mut ancestor = node.parent();
while let Some(conditional) = ancestor {
if matches!(conditional.kind(), "preproc_if" | "preproc_ifdef")
&& !is_file_covering_include_guard(conditional, source)
&& !is_split_cpp_language_linkage_wrapper(conditional, node, source)
&& preprocessor_conditional_contains_descendant_with_cache(conditional, node, cache)
{
guards.push(preprocessor_guard_for_descendant(
conditional,
node,
source,
cache,
)?);
}
ancestor = conditional.parent();
}
Some(guards)
}
fn fragmented_statement_preprocessor_guard(
descendant: Node<'_>,
source: &str,
) -> Option<PreprocessorGuard> {
let mut ancestor = descendant.parent();
while let Some(statement) = ancestor {
if statement.kind() == "if_statement"
&& let (Some(consequence), Some(alternative)) = (
statement.child_by_field_name("consequence"),
statement.child_by_field_name("alternative"),
)
&& alternative.start_byte() <= descendant.start_byte()
&& descendant.end_byte() <= alternative.end_byte()
{
let mut cursor = consequence.walk();
let openers = consequence
.named_children(&mut cursor)
.filter(|child| {
matches!(child.kind(), "preproc_if" | "preproc_ifdef")
&& child
.child(child.child_count().saturating_sub(1))
.is_some_and(|last| last.kind() == "#endif" && last.is_missing())
})
.collect::<Vec<_>>();
if openers.len() != 1 {
ancestor = statement.parent();
continue;
}
let mut terminators = Vec::new();
let mut stack = vec![alternative];
while let Some(node) = stack.pop() {
if node.kind() == "preproc_call"
&& node.start_byte() >= descendant.end_byte()
&& node
.child_by_field_name("directive")
.is_some_and(|directive| node_text(directive, source).trim() == "#endif")
{
terminators.push(node);
continue;
}
push_named_children_reversed(node, &mut stack);
}
if terminators.len() == 1 {
return simple_preprocessor_guard(openers[0], source);
}
}
ancestor = statement.parent();
}
None
}
pub(crate) fn preprocessor_guard_for_descendant(
conditional: Node<'_>,
descendant: Node<'_>,
source: &str,
cache: &mut PreprocessorContextCache,
) -> Option<PreprocessorGuard> {
let guard = simple_preprocessor_guard(conditional, source)?;
match preprocessor_branch_of_descendant(conditional, descendant, cache) {
PreprocessorBranch::Consequence => Some(guard),
PreprocessorBranch::Structured(alternative)
if matches!(alternative.kind(), "preproc_else" | "preproc_elif") =>
{
Some(guard.negated())
}
PreprocessorBranch::DisplacedElse => Some(guard.negated()),
PreprocessorBranch::Structured(_) | PreprocessorBranch::Unreadable => None,
}
}
enum PreprocessorBranch<'tree> {
Consequence,
Structured(Node<'tree>),
DisplacedElse,
Unreadable,
}
fn preprocessor_branch_of_descendant<'tree>(
conditional: Node<'tree>,
descendant: Node<'_>,
cache: &mut PreprocessorContextCache,
) -> PreprocessorBranch<'tree> {
if let Some(alternative) =
conditional
.child_by_field_name("alternative")
.filter(|alternative| {
alternative.start_byte() <= descendant.start_byte()
&& descendant.end_byte() <= alternative.end_byte()
})
{
return PreprocessorBranch::Structured(alternative);
}
match cache.displaced_branch(conditional) {
Some(directive) if directive.end_byte <= descendant.start_byte() => {
if directive.is_else {
PreprocessorBranch::DisplacedElse
} else {
PreprocessorBranch::Unreadable
}
}
_ => PreprocessorBranch::Consequence,
}
}
pub(crate) fn preprocessor_conditional_contains_descendant(
conditional: Node<'_>,
descendant: Node<'_>,
) -> bool {
cpp_displaced_preprocessor_boundary(conditional)
.is_none_or(|boundary| descendant.end_byte() <= boundary.end_byte)
}
fn preprocessor_conditional_contains_descendant_with_cache(
conditional: Node<'_>,
descendant: Node<'_>,
cache: &mut PreprocessorContextCache,
) -> bool {
cache
.displaced_boundary_end(conditional)
.is_none_or(|end_byte| descendant.end_byte() <= end_byte)
}
pub fn merge_preprocessor_guards(
left: &HashSet<PreprocessorGuard>,
right: &HashSet<PreprocessorGuard>,
) -> Option<HashSet<PreprocessorGuard>> {
let mut merged = left.clone();
for guard in right {
let boolean_negation = guard.as_boolean_expression().negated();
if merged.contains(&guard.negated())
|| merged
.iter()
.map(PreprocessorGuard::as_boolean_expression)
.any(|existing| existing == boolean_negation)
{
return None;
}
merged.insert(guard.clone());
}
Some(merged)
}
fn simple_preprocessor_guard(conditional: Node<'_>, source: &str) -> Option<PreprocessorGuard> {
if conditional.kind() == "preproc_ifdef" {
let name = conditional.child_by_field_name("name")?;
let name = node_text(name, source).to_string();
return match conditional.child(0)?.kind() {
"#ifdef" => Some(PreprocessorGuard::Defined(name)),
"#ifndef" => Some(PreprocessorGuard::Undefined(name)),
_ => None,
};
}
let condition = conditional.child_by_field_name("condition")?;
simple_preprocessor_expression_guard(condition, source).or_else(|| {
Some(PreprocessorGuard::Expression(normalize_cpp_whitespace(
node_text(condition, source),
)))
})
}
fn simple_preprocessor_expression_guard(
expression: Node<'_>,
source: &str,
) -> Option<PreprocessorGuard> {
match expression.kind() {
"identifier" => Some(PreprocessorGuard::Boolean(BooleanGuardExpression::Truthy(
node_text(expression, source).to_string(),
))),
"number_literal" => match node_text(expression, source).trim() {
"0" => Some(PreprocessorGuard::Constant(false)),
"1" => Some(PreprocessorGuard::Constant(true)),
_ => None,
},
"preproc_defined" => {
let identifier = (0..expression.named_child_count())
.filter_map(|index| expression.named_child(index))
.find(|child| child.kind() == "identifier")?;
Some(PreprocessorGuard::Defined(
node_text(identifier, source).to_string(),
))
}
"unary_expression"
if expression
.child_by_field_name("operator")
.is_some_and(|operator| operator.kind() == "!") =>
{
simple_preprocessor_expression_guard(
expression.child_by_field_name("argument")?,
source,
)
.map(|guard| guard.negated())
}
"parenthesized_expression" => (0..expression.named_child_count())
.filter_map(|index| expression.named_child(index))
.next()
.and_then(|child| simple_preprocessor_expression_guard(child, source)),
"binary_expression" => Some(PreprocessorGuard::Boolean(boolean_preprocessor_expression(
expression, source,
))),
_ => None,
}
}
fn boolean_preprocessor_expression(expression: Node<'_>, source: &str) -> BooleanGuardExpression {
match expression.kind() {
"number_literal" => match node_text(expression, source).trim() {
"0" => BooleanGuardExpression::Constant(false),
"1" => BooleanGuardExpression::Constant(true),
_ => BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(
expression, source,
))),
},
"identifier" => BooleanGuardExpression::Truthy(node_text(expression, source).to_string()),
"preproc_defined" => {
let identifier = (0..expression.named_child_count())
.filter_map(|index| expression.named_child(index))
.find(|child| child.kind() == "identifier");
identifier.map_or_else(
|| {
BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(
expression, source,
)))
},
|identifier| {
BooleanGuardExpression::Defined(node_text(identifier, source).to_string())
},
)
}
"unary_expression"
if expression
.child_by_field_name("operator")
.is_some_and(|operator| operator.kind() == "!") =>
{
expression.child_by_field_name("argument").map_or_else(
|| {
BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(
expression, source,
)))
},
|argument| boolean_preprocessor_expression(argument, source).negated(),
)
}
"parenthesized_expression" => (0..expression.named_child_count())
.filter_map(|index| expression.named_child(index))
.next()
.map_or_else(
|| {
BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(
expression, source,
)))
},
|child| boolean_preprocessor_expression(child, source),
),
"binary_expression" => {
let operands = || {
Some((
boolean_preprocessor_expression(
expression.child_by_field_name("left")?,
source,
),
boolean_preprocessor_expression(
expression.child_by_field_name("right")?,
source,
),
))
};
match expression
.child_by_field_name("operator")
.map(|operator| operator.kind())
{
Some("&&") => operands().map_or_else(
|| {
BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(
expression, source,
)))
},
|(left, right)| BooleanGuardExpression::all([left, right]),
),
Some("||") => operands().map_or_else(
|| {
BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(
expression, source,
)))
},
|(left, right)| BooleanGuardExpression::any([left, right]),
),
_ => BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(
expression, source,
))),
}
}
_ => {
BooleanGuardExpression::Opaque(normalize_cpp_whitespace(node_text(expression, source)))
}
}
}
fn unique_include_target(mut targets: Vec<ProjectFile>) -> Option<ProjectFile> {
if targets.len() == 1 {
targets.pop()
} else {
None
}
}
fn callable_declaration_nodes<'tree>(
analyzer: &CppGraphSource<'_>,
prepared: &'tree PreparedSyntaxTree,
candidate: &CodeUnit,
) -> Vec<Node<'tree>> {
let root = prepared.tree().root_node();
analyzer
.ranges(candidate)
.into_iter()
.filter_map(|range| {
let mut declaration =
root.descendant_for_byte_range(range.start_byte, range.end_byte)?;
while !matches!(
declaration.kind(),
"declaration" | "field_declaration" | "function_definition"
) && !crate::declarations::is_macro_wrapped_declaration_envelope(
declaration,
prepared.source(),
) {
let Some(parent) = declaration.parent() else {
break;
};
declaration = parent;
}
Some(declaration)
})
.collect()
}
fn real_function_definition_ancestor<'tree>(
node: Node<'tree>,
source: &str,
) -> Option<Node<'tree>> {
let mut ancestor = node.parent();
while let Some(node) = ancestor {
if node.kind() == "function_definition"
&& !is_recovered_declaration_scope_container(node, source)
{
return Some(node);
}
ancestor = node.parent();
}
None
}
fn callable_declaration_activation_in_file(
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
reference: &CallableReferenceContext<'_>,
) -> Option<usize> {
analyzer
.declaration_source_occurrences(candidate)?
.into_iter()
.filter_map(|occurrence| {
let activation = occurrence.fact.callable_activation?;
let guards = cpp_guard_set_to_runtime(occurrence.fact.callable_guards.as_ref()?)?;
callable_guard_requirements_are_visible(
&guards,
occurrence.fact.callable_guard_completion_byte,
reference,
)
.then_some(activation)
})
.min()
}
fn callable_guard_requirements_are_visible(
required: &HashSet<PreprocessorGuard>,
completion_byte: Option<usize>,
reference: &CallableReferenceContext<'_>,
) -> bool {
if required.is_empty() || guard_requirements_hold_at_reference(required, reference.guards()) {
return true;
}
reference.position.as_ref().is_some_and(|position| {
completion_byte.is_some_and(|end| end > 0 && end <= position.byte)
&& guards_compatible_at_reference(required, reference.guards())
})
}
type ReferenceGuardAugmenter<'a> =
dyn Fn(Option<&HashSet<PreprocessorGuard>>) -> Option<HashSet<PreprocessorGuard>> + 'a;
struct CallableReferenceContext<'a> {
effective_guards: &'a ReferenceGuardAugmenter<'a>,
file: &'a ProjectFile,
position: Option<CallableReferencePosition<'a>>,
file_guards: OnceCell<Option<HashSet<PreprocessorGuard>>>,
}
struct CallableReferencePosition<'a> {
prepared: &'a PreparedSyntaxTree,
byte: usize,
guards: &'a OnceCell<Option<HashSet<PreprocessorGuard>>>,
}
impl CallableReferenceContext<'_> {
fn guards(&self) -> Option<&HashSet<PreprocessorGuard>> {
let Some(position) = self.position.as_ref() else {
return self
.file_guards
.get_or_init(|| (self.effective_guards)(Some(&HashSet::default())))
.as_ref();
};
position
.guards
.get_or_init(|| {
let raw = position
.prepared
.tree()
.root_node()
.descendant_for_byte_range(position.byte, position.byte.saturating_add(1))
.and_then(|node| {
preprocessor_guard_environment(node, position.prepared.source())
});
(self.effective_guards)(raw.as_ref())
})
.as_ref()
}
}
fn callable_declaration_guard_requirements(
node: Node<'_>,
source: &str,
reference: &CallableReferenceContext<'_>,
) -> Option<HashSet<PreprocessorGuard>> {
let mut required = HashSet::default();
for guard in callable_preprocessor_ancestor_guards(node, source)? {
match guard {
PreprocessorGuard::Constant(true) => {}
PreprocessorGuard::Constant(false) => return None,
guard => {
required.insert(guard);
}
}
}
guards_compatible_at_reference(&required, reference.guards()).then_some(required)
}
fn callable_preprocessor_context_is_visible_for_reference(
node: Node<'_>,
source: &str,
reference: &CallableReferenceContext<'_>,
) -> bool {
let Some(required) = callable_declaration_guard_requirements(node, source, reference) else {
return false;
};
if required.is_empty() || guard_requirements_hold_at_reference(&required, reference.guards()) {
return true;
}
reference.position.as_ref().is_some_and(|position| {
guards_compatible_at_reference(&required, reference.guards())
&& declaration_follows_completed_family_before_reference(node, source, position.byte)
})
}
fn flattened_macro_namespace_declaration_matches(
analyzer: &CppGraphSource<'_>,
_cpp: &dyn CppSource,
reference_file: &ProjectFile,
visible_declaration: &CodeUnit,
qualified_candidate: &CodeUnit,
reference_byte: usize,
) -> bool {
if visible_declaration.kind() != qualified_candidate.kind()
|| visible_declaration.identifier() != qualified_candidate.identifier()
|| visible_declaration.signature() != qualified_candidate.signature()
|| !visible_declaration.package_name().is_empty()
|| qualified_candidate.package_name().is_empty()
{
return false;
}
let Some(occurrences) = analyzer.declaration_source_occurrences(visible_declaration) else {
return false;
};
occurrences.into_iter().any(|occurrence| {
occurrence
.displaced_namespace_closing_brace
.is_some_and(|closing_brace| {
visible_declaration.source() != reference_file || closing_brace < reference_byte
})
})
}
pub(crate) fn flattened_macro_namespace_components(
declaration: Node<'_>,
source: &str,
) -> Option<Vec<String>> {
flattened_macro_function_namespace_components(declaration, source)
.or_else(|| flattened_macro_error_namespace_components(declaration, source))
}
fn flattened_macro_function_namespace_components(
declaration: Node<'_>,
source: &str,
) -> Option<Vec<String>> {
let body = declaration
.parent()
.filter(|parent| parent.kind() == "compound_statement")?;
let function = body.parent()?;
if function.child_by_field_name("body") != Some(body) {
return None;
}
let namespace_name = recovered_macro_namespace_name(function, source)?;
let mut components = enclosing_namespace_components(declaration, source)?;
components.push(namespace_name);
Some(components)
}
fn recovered_macro_namespace_name(function: Node<'_>, source: &str) -> Option<String> {
if function.kind() != "function_definition" {
return None;
}
let body = function
.child_by_field_name("body")
.filter(|body| body.kind() == "compound_statement")?;
let mut cursor = function.walk();
let prefix = function
.named_children(&mut cursor)
.take_while(|child| child.start_byte() < body.start_byte())
.filter(|child| child.kind() != "comment")
.collect::<Vec<_>>();
let begin_index = prefix.iter().rposition(|child| {
flattened_macro_sentinel_name(*child, source)
.is_some_and(|name| is_namespace_begin_sentinel(&name))
})?;
let mut identifiers = Vec::new();
let mut stack = prefix[begin_index + 1..]
.iter()
.rev()
.copied()
.collect::<Vec<_>>();
while let Some(current) = stack.pop() {
if let Some(identifier) = direct_cpp_identifier_name(current, source) {
identifiers.push(identifier);
continue;
}
let mut cursor = current.walk();
let children = current.named_children(&mut cursor).collect::<Vec<_>>();
stack.extend(children.into_iter().rev());
}
let [keyword, namespace_name] = identifiers.as_slice() else {
return None;
};
if keyword != "namespace" || namespace_name.is_empty() {
return None;
}
let mut next = function.next_named_sibling();
let next = loop {
let candidate = next?;
next = candidate.next_named_sibling();
if candidate.kind() != "comment" {
break candidate;
}
};
flattened_macro_sentinel_name(next, source)
.is_some_and(|name| is_namespace_end_sentinel(&name))
.then(|| namespace_name.clone())
}
pub(crate) fn is_recovered_declaration_scope_container(node: Node<'_>, source: &str) -> bool {
crate::declarations::is_recovered_exported_class_container(node, source)
|| crate::declarations::is_recovered_fragmented_partial_specialization_container(
node, source,
)
|| recovered_macro_namespace_name(node, source).is_some()
}
fn flattened_macro_error_namespace_components(
declaration: Node<'_>,
source: &str,
) -> Option<Vec<String>> {
let parent = declaration
.parent()
.filter(|parent| parent.kind() == "ERROR" && parent.has_error())?;
let mut cursor = parent.walk();
let siblings = parent.named_children(&mut cursor).collect::<Vec<_>>();
let declaration_index = siblings
.iter()
.position(|candidate| same_node(*candidate, declaration))?;
let begin_index = (0..declaration_index).rev().find(|index| {
flattened_macro_sentinel_name(siblings[*index], source)
.is_some_and(|name| is_namespace_begin_sentinel(&name))
})?;
let significant = siblings[begin_index + 1..declaration_index]
.iter()
.copied()
.filter(|node| node.kind() != "comment")
.collect::<Vec<_>>();
let [namespace_keyword, namespace_name, ..] = significant.as_slice() else {
return None;
};
if direct_cpp_identifier_name(*namespace_keyword, source).as_deref() != Some("namespace") {
return None;
}
let namespace_name = flattened_macro_namespace_name(*namespace_name, source)?;
if significant[2..].iter().any(|node| {
flattened_macro_sentinel_name(*node, source).is_some_and(|name| {
is_namespace_begin_sentinel(&name) || is_namespace_end_sentinel(&name)
})
}) {
return None;
}
let mut saw_namespace_close = false;
for sibling in siblings.iter().skip(declaration_index + 1).copied() {
if sibling.kind() == "comment" {
continue;
}
if !saw_namespace_close {
if direct_unmatched_closing_brace(sibling) {
saw_namespace_close = true;
continue;
}
if flattened_macro_sentinel_name(sibling, source).is_some() {
return None;
}
continue;
}
if !flattened_macro_sentinel_name(sibling, source)
.is_some_and(|name| is_namespace_end_sentinel(&name))
{
return None;
}
let mut components = enclosing_namespace_components(declaration, source)?;
components.push(namespace_name);
return Some(components);
}
None
}
fn flattened_macro_sentinel_name(node: Node<'_>, source: &str) -> Option<String> {
let node = if node.kind() == "expression_statement" && node.named_child_count() == 1 {
node.named_child(0)?
} else {
node
};
let candidates = [
direct_cpp_identifier_name(node, source),
node.child_by_field_name("type")
.and_then(|type_node| direct_cpp_identifier_name(type_node, source)),
macro_modifier_identifier(node)
.map(|identifier| normalize_cpp_whitespace(node_text(identifier, source))),
{
let mut cursor = node.walk();
node.named_children(&mut cursor)
.find(|child| child.kind() != "comment")
.and_then(macro_modifier_identifier)
.map(|identifier| normalize_cpp_whitespace(node_text(identifier, source)))
},
];
candidates.into_iter().flatten().find(|candidate| {
cpp_export_macro_token(candidate)
&& (is_namespace_begin_sentinel(candidate) || is_namespace_end_sentinel(candidate))
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum NamespaceSentinel {
Begin,
End,
}
fn namespace_sentinel(name: &str) -> Option<NamespaceSentinel> {
if name.ends_with("NAMESPACE_BEGIN") || name.ends_with("BEGIN_NAMESPACE") {
Some(NamespaceSentinel::Begin)
} else if name.ends_with("NAMESPACE_END") || name.ends_with("END_NAMESPACE") {
Some(NamespaceSentinel::End)
} else {
None
}
}
fn is_namespace_begin_sentinel(name: &str) -> bool {
namespace_sentinel(name) == Some(NamespaceSentinel::Begin)
}
fn is_namespace_end_sentinel(name: &str) -> bool {
namespace_sentinel(name) == Some(NamespaceSentinel::End)
}
fn flattened_macro_namespace_name(node: Node<'_>, source: &str) -> Option<String> {
if node.kind() != "ERROR" || node.named_child_count() != 1 {
return None;
}
let name = direct_cpp_identifier_name(node.named_child(0)?, source)?;
(!cpp_export_macro_token(&name)).then_some(name)
}
fn direct_cpp_identifier_name(node: Node<'_>, source: &str) -> Option<String> {
if !matches!(
node.kind(),
"identifier" | "namespace_identifier" | "type_identifier"
) {
return None;
}
let name = normalize_cpp_whitespace(node_text(node, source));
(!name.is_empty()).then_some(name)
}
fn guard_requirement_sets_match(
left: &[(usize, HashSet<PreprocessorGuard>)],
right: &[(usize, HashSet<PreprocessorGuard>)],
) -> bool {
left.len() == right.len()
&& left.iter().all(|(_, left_guards)| {
right
.iter()
.any(|(_, right_guards)| left_guards == right_guards)
})
&& right.iter().all(|(_, right_guards)| {
left.iter()
.any(|(_, left_guards)| right_guards == left_guards)
})
}
pub(crate) fn direct_unmatched_closing_brace(node: Node<'_>) -> bool {
node.kind() == "ERROR"
&& (0..node.child_count())
.any(|index| node.child(index).is_some_and(|child| child.kind() == "}"))
}
pub fn binding_site_guards(node: Node<'_>, source: &str) -> Option<HashSet<PreprocessorGuard>> {
if callable_preprocessor_context_is_visible(node, source) {
Some(HashSet::default())
} else {
preprocessor_guard_environment(node, source)
}
}
pub fn callable_preprocessor_context_is_visible(node: Node<'_>, source: &str) -> bool {
let mut ancestor = node.parent();
while let Some(parent) = ancestor {
if is_cpp_preprocessor_conditional(parent)
&& !is_file_covering_include_guard(parent, source)
&& !is_split_cpp_language_linkage_wrapper(parent, node, source)
{
return false;
}
ancestor = parent.parent();
}
true
}
fn is_split_cpp_language_linkage_wrapper(
conditional: Node<'_>,
descendant: Node<'_>,
source: &str,
) -> bool {
if conditional.child_by_field_name("alternative").is_some()
|| !matches!(
simple_preprocessor_guard(conditional, source),
Some(PreprocessorGuard::Defined(name)) if name == "__cplusplus"
)
{
return false;
}
let mut current = descendant.parent();
let linkage = loop {
let Some(node) = current else {
return false;
};
if node == conditional {
return false;
}
if node.kind() == "linkage_specification" {
break node;
}
current = node.parent();
};
if linkage
.child_by_field_name("value")
.is_none_or(|value| node_text(value, source) != "\"C\"")
{
return false;
}
let Some(body) = linkage.child_by_field_name("body") else {
return false;
};
let mut cursor = body.walk();
let closes_opening_branch = body
.named_children(&mut cursor)
.take_while(|child| child.end_byte() <= descendant.start_byte())
.any(|child| {
child.kind() == "preproc_call"
&& child
.child_by_field_name("directive")
.is_some_and(|directive| node_text(directive, source) == "#endif")
});
let mut cursor = body.walk();
let reopens_for_closing_brace = body
.named_children(&mut cursor)
.skip_while(|child| child.start_byte() < descendant.end_byte())
.any(|child| {
matches!(
simple_preprocessor_guard(child, source),
Some(PreprocessorGuard::Defined(name)) if name == "__cplusplus"
) && {
let mut tokens = child.walk();
child
.children(&mut tokens)
.any(|token| token.kind() == "#endif" && token.is_missing())
}
});
closes_opening_branch && reopens_for_closing_brace
}
pub fn call_arguments_node(node: Node<'_>) -> Option<Node<'_>> {
node.child_by_field_name("arguments")
.or_else(|| node.child_by_field_name("parameters"))
.or_else(|| node.child_by_field_name("value"))
.or_else(|| first_named_child_of_kind(node, "argument_list"))
.or_else(|| first_named_child_of_kind(node, "initializer_list"))
}
pub fn call_arity(node: Node<'_>) -> usize {
call_arguments_node(node)
.map(|args| argument_children(args).count())
.unwrap_or(0)
}
pub fn argument_children<'tree>(node: Node<'tree>) -> impl Iterator<Item = Node<'tree>> {
let recovered_block_arguments = recovered_block_literal_arguments(node);
(0..node.child_count())
.filter_map(move |index| node.child(index))
.filter(|child| child.is_named() && !child.is_extra())
.filter(move |child| node.kind() != "parameter_list" || !block_literal_open_error(*child))
.flat_map(move |child| {
if let Some((raw, left, right)) = recovered_block_arguments
&& child == raw
{
[Some(left), Some(right)]
} else {
[Some(child), None]
}
})
.flatten()
}
fn block_literal_open_error(node: Node<'_>) -> bool {
if node.kind() != "ERROR" || node.is_missing() {
return false;
}
let mut cursor = node.walk();
let tokens = node.children(&mut cursor).collect::<Vec<_>>();
matches!(tokens.as_slice(), [caret, open] if caret.kind() == "^" && open.kind() == "(")
}
pub fn recovered_c_new_expression_arguments(
node: Node<'_>,
uses_c_semantics: bool,
) -> Option<[Node<'_>; 2]> {
if !uses_c_semantics || node.kind() != "new_expression" {
return None;
}
let parent = node.parent()?;
if parent.kind() != "argument_list" {
return None;
}
let keyword = node.child(0)?;
let error = node.child(1)?;
let trailing = node.child(2)?;
if node.child(3).is_some()
|| keyword.kind() != "new"
|| keyword.is_named()
|| keyword.child_count() != 0
|| error.kind() != "ERROR"
|| !error.is_extra()
|| error.child_count() != 1
|| error.child(0).is_none_or(|comma| comma.kind() != ",")
|| node.child_by_field_name("type") != Some(trailing)
|| trailing.kind() != "type_identifier"
{
return None;
}
Some([keyword, trailing])
}
pub fn recovered_c_new_expression_argument_at(
mut node: Node<'_>,
start_byte: usize,
end_byte: usize,
uses_c_semantics: bool,
) -> Option<Node<'_>> {
if !uses_c_semantics {
return None;
}
loop {
if let Some(arguments) = recovered_c_new_expression_arguments(node, uses_c_semantics) {
return arguments.into_iter().find(|argument| {
argument.start_byte() <= start_byte && end_byte <= argument.end_byte()
});
}
node = node.parent()?;
}
}
fn recovered_c_keyword_argument_count(
file: &ProjectFile,
call: Node<'_>,
arguments: Node<'_>,
source: &str,
) -> usize {
if !is_c_source_file(file) || arguments.kind() != "argument_list" {
return 0;
}
let mut ancestor = Some(call);
let function = loop {
let Some(current) = ancestor else {
return recovered_c_keyword_argument_count_from_collapsed_function(call, arguments)
.unwrap_or(0);
};
if current.kind() == "function_definition" {
break current;
}
ancestor = current.parent();
};
let Some(parameters) = function
.child_by_field_name("declarator")
.and_then(|declarator| declarator.child_by_field_name("parameters"))
else {
return 0;
};
let displaced_parameter_keywords = (0..parameters.child_count())
.filter_map(|index| parameters.child(index))
.filter(|error| error.kind() == "ERROR")
.filter_map(|error| {
let parameter = error.prev_named_sibling()?;
if parameter.kind() != "parameter_declaration"
|| parameter.end_byte() != error.start_byte()
|| extract_variable_name(parameter, source).is_some()
{
return None;
}
let mut children = (0..error.child_count())
.filter_map(|index| error.child(index))
.filter(|child| !child.is_extra() && !child.is_missing());
let keyword = children.next()?;
(children.next().is_none() && !keyword.is_named() && keyword.child_count() == 0)
.then_some(keyword)
})
.collect::<Vec<_>>();
if displaced_parameter_keywords.is_empty() {
return 0;
}
(0..arguments.child_count())
.filter_map(|index| arguments.child(index))
.filter(|error| error.kind() == "ERROR" && error.is_extra())
.filter(|error| {
let mut children = (0..error.child_count())
.filter_map(|index| error.child(index))
.filter(|child| !child.is_extra() && !child.is_missing());
let Some(comma) = children.next() else {
return false;
};
let Some(keyword) = children.next() else {
return false;
};
children.next().is_none()
&& comma.kind() == ","
&& !keyword.is_named()
&& keyword.child_count() == 0
&& displaced_parameter_keywords
.iter()
.any(|parameter| parameter.kind_id() == keyword.kind_id())
})
.count()
}
fn recovered_c_keyword_argument_count_from_collapsed_function(
call: Node<'_>,
arguments: Node<'_>,
) -> Option<usize> {
let argument_keywords = children_iter(arguments)
.filter(|error| error.kind() == "ERROR" && error.is_extra())
.filter_map(|error| {
let mut children =
children_iter(error).filter(|child| !child.is_extra() && !child.is_missing());
let comma = children.next()?;
let keyword = children.next()?;
(children.next().is_none()
&& comma.kind() == ","
&& !keyword.is_named()
&& keyword.child_count() == 0)
.then_some(keyword)
})
.collect::<Vec<_>>();
if argument_keywords.is_empty() {
return Some(0);
}
let mut ancestor = call.parent();
let body = loop {
let current = ancestor?;
if matches!(current.kind(), "function_definition" | "lambda_expression") {
return None;
}
if current.kind() == "compound_statement"
&& current
.parent()
.is_some_and(|parent| parent.kind() == "ERROR")
{
break current;
}
ancestor = current.parent();
};
let parameter_error = body.parent().filter(|parent| parent.kind() == "ERROR")?;
let function_error = parameter_error
.parent()
.filter(|parent| parent.kind() == "ERROR")?;
let function_children = children_iter(function_error).collect::<Vec<_>>();
let parameter_error_index = function_children
.iter()
.position(|child| same_node(*child, parameter_error))?;
let has_return_type = function_children[..parameter_error_index.saturating_sub(2)]
.iter()
.enumerate()
.any(|(index, child)| {
function_error.field_name_for_child(index as u32) == Some("type")
&& !child.is_extra()
&& !child.is_missing()
&& matches!(
child.kind(),
"primitive_type" | "sized_type_specifier" | "type_identifier"
)
});
if parameter_error_index < 2
|| !has_return_type
|| function_children[parameter_error_index - 1].kind() != "("
|| function_children[parameter_error_index - 2].kind() != "identifier"
{
return Some(0);
}
let parameter_children = children_iter(parameter_error).collect::<Vec<_>>();
let body_index = parameter_children
.iter()
.position(|child| same_node(*child, body))?;
let close_parenthesis_index = parameter_children[..body_index]
.iter()
.position(|child| child.kind() == ")")?;
let parameter_keywords = parameter_children[..close_parenthesis_index]
.iter()
.enumerate()
.filter(|(index, child)| {
!child.is_named()
&& !child.is_extra()
&& !child.is_missing()
&& child.child_count() == 0
&& argument_keywords
.iter()
.any(|keyword| keyword.kind_id() == child.kind_id())
&& collapsed_c_parameter_name_position(¶meter_children, *index)
})
.map(|(_, child)| *child)
.collect::<Vec<_>>();
Some(
argument_keywords
.iter()
.filter(|keyword| {
parameter_keywords
.iter()
.any(|parameter| parameter.kind_id() == keyword.kind_id())
})
.count(),
)
}
fn collapsed_c_parameter_name_position(children: &[Node<'_>], keyword_index: usize) -> bool {
let segment_start = children[..keyword_index]
.iter()
.rposition(|child| matches!(child.kind(), "(" | ","))
.map_or(0, |index| index + 1);
let mut saw_type = false;
let mut saw_pointer = false;
for child in &children[segment_start..keyword_index] {
if child.kind() == "*" && !child.is_extra() && !child.is_missing() {
saw_pointer = true;
continue;
}
if saw_pointer {
return false;
}
if !child.is_named() || child.is_extra() || child.is_missing() {
return false;
}
if child.kind() == "type_qualifier" {
continue;
}
if matches!(
child.kind(),
"class_specifier"
| "dependent_type"
| "enum_specifier"
| "identifier"
| "primitive_type"
| "qualified_identifier"
| "scoped_identifier"
| "struct_specifier"
| "template_type"
| "type_identifier"
| "union_specifier"
) {
saw_type = true;
continue;
}
return false;
}
saw_type && saw_pointer
}
fn recovered_block_literal_arguments<'tree>(
arguments: Node<'tree>,
) -> Option<(Node<'tree>, Node<'tree>, Node<'tree>)> {
if arguments.kind() != "argument_list" {
return None;
}
let mut raw_arguments = (0..arguments.child_count())
.filter_map(|index| arguments.child(index))
.filter(|child| child.is_named() && !child.is_extra());
let raw = raw_arguments.next()?;
if raw_arguments.next().is_some() || raw.kind() != "binary_expression" {
return None;
}
let left = raw.child_by_field_name("left")?;
if left.is_missing() || left.start_byte() == left.end_byte() {
return None;
}
let right = raw.child_by_field_name("right")?;
if right.kind() != "compound_literal_expression"
|| right.is_missing()
|| right
.child_by_field_name("type")
.is_none_or(|node| node.kind() != "type_descriptor" || node.is_missing())
|| right
.child_by_field_name("value")
.is_none_or(|node| node.kind() != "initializer_list" || node.is_missing())
{
return None;
}
let has_intervening_error = (0..raw.child_count())
.filter_map(|index| raw.child(index))
.any(|child| {
child.kind() == "ERROR"
&& !child.is_missing()
&& child.start_byte() >= left.end_byte()
&& child.end_byte() <= right.start_byte()
});
has_intervening_error.then_some((raw, left, right))
}
pub fn constructor_type_node(node: Node<'_>) -> Option<Node<'_>> {
match node.kind() {
"new_expression" => node
.child_by_field_name("type")
.or_else(|| node.named_child(0)),
"compound_literal_expression" => node.child_by_field_name("type"),
"call_expression" => node.child_by_field_name("function"),
_ => None,
}
}
pub fn cast_expression_type_node(node: Node<'_>) -> Option<Node<'_>> {
if node.kind() != "cast_expression" {
return None;
}
let descriptor = node.child_by_field_name("type")?;
if descriptor.kind() == "type_descriptor" {
descriptor.child_by_field_name("type")
} else {
Some(descriptor)
}
}
pub fn cpp_field_expression_receiver(field: Node<'_>) -> Option<Node<'_>> {
debug_assert_eq!(field.kind(), "field_expression");
let operator = field.child_by_field_name("operator")?;
let mut cursor = field.walk();
let receiver = field
.named_children(&mut cursor)
.filter(|child| child.end_byte() <= operator.start_byte())
.last()?;
if receiver.kind() != "ERROR" {
return Some(receiver);
}
(receiver.named_child_count() == 1)
.then(|| receiver.named_child(0))
.flatten()
}
pub fn field_initializer_constructs_target(
node: Node<'_>,
ctx: &ScanCtx<'_>,
owner: &CodeUnit,
) -> bool {
if first_named_child_of_kind(node, "qualified_identifier").is_some() {
return qualified_base_initializer_constructs_target(node, ctx, owner);
}
let Some(name) = node
.child_by_field_name("name")
.or_else(|| first_named_child_of_kind(node, "field_identifier"))
.or_else(|| first_named_child_of_kind(node, "qualified_identifier"))
else {
return false;
};
let field_name = node_text(name, ctx.source);
ctx.visibility
.visible_identifier_candidates(ctx.file, field_name)
.filter(|unit| unit.is_field() && unit.identifier() == field_name)
.any(|unit| field_declares_type(unit, ctx, owner) && field_holds_subobject(unit, ctx))
}
fn field_holds_subobject(unit: &CodeUnit, ctx: &ScanCtx<'_>) -> bool {
field_initializer_holds_subobject(&ctx.analyzer, ctx.visibility, unit)
}
fn qualified_base_initializer_constructs_target(
node: Node<'_>,
ctx: &ScanCtx<'_>,
owner: &CodeUnit,
) -> bool {
let Some(qualified) = first_named_child_of_kind(node, "qualified_identifier") else {
return false;
};
let Some(components) = cpp_type_name_components(qualified, ctx.source) else {
return false;
};
let Some(lexical_scope) = enclosing_namespace_components(node, ctx.source) else {
return false;
};
let resolves_target = |components: &[String]| {
matches!(
ctx.visibility.resolve_type_components_lexically_for_target(
&ctx.analyzer,
ctx.file,
components,
is_globally_qualified_cpp_name(qualified),
&lexical_scope,
owner,
),
LexicalTypeResolution::Resolved { unit, .. }
if same_visible_symbol(&unit, owner)
)
};
if resolves_target(&components) {
return true;
}
components
.last()
.is_some_and(|terminal| terminal == owner.identifier())
&& resolves_target(&components[..components.len() - 1])
}
fn field_declares_type(unit: &CodeUnit, ctx: &ScanCtx<'_>, owner: &CodeUnit) -> bool {
ctx.visibility
.field_declared_type_fact(&ctx.analyzer, unit)
.is_some_and(|fact| {
ctx.visibility
.resolves_to_type(&ctx.analyzer, ctx.file, &fact.type_text, owner)
|| ctx.visibility.resolves_to_type(
&ctx.analyzer,
ctx.file,
&normalize_field_type_text(&fact.type_text),
owner,
)
})
}
pub fn field_declared_binding(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
visible_from: &ProjectFile,
field: &CodeUnit,
) -> Option<CppScanBinding> {
let (normalized, resolved, indirection) =
field_declared_type_binding(analyzer, visibility, visible_from, field)?;
let resolved = resolved
.or_else(|| anonymous_aggregate_field_owner(analyzer, visibility, visible_from, field));
Some(CppScanBinding::from_type_name(
normalized,
resolved,
indirection,
))
}
pub fn field_initializer_holds_subobject(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
field: &CodeUnit,
) -> bool {
visibility
.field_declared_type_fact(analyzer, field)
.is_none_or(|fact| !fact.binds_indirectly)
}
fn anonymous_aggregate_field_owner(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
visible_from: &ProjectFile,
field: &CodeUnit,
) -> Option<CodeUnit> {
let owner = type_owner_of(analyzer, field)?;
if !owner.is_class() {
return None;
}
let declaration = analyzer.get_source(field, false)?;
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.ok()?;
let tree = parser.parse(&declaration, None)?;
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
if matches!(node.kind(), "declaration" | "field_declaration")
&& let Some(type_node) = node
.child_by_field_name("type")
.or_else(|| first_type_child(node))
&& matches!(type_node.kind(), "struct_specifier" | "union_specifier")
&& type_node.child_by_field_name("name").is_none()
&& declared_name_binding(node, type_node, field.identifier(), &declaration).is_some()
{
let matches = visibility
.visible_members_for_owner_name(visible_from, &owner, field.identifier())
.into_iter()
.filter(|child| child.is_class() && child.identifier() == field.identifier())
.collect::<Vec<_>>();
return match matches.as_slice() {
[child] => Some((*child).clone()),
_ => None,
};
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
None
}
pub fn anonymous_aggregate_owner(
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
node: Node<'_>,
) -> Option<CodeUnit> {
if !matches!(node.kind(), "struct_specifier" | "union_specifier")
|| node.child_by_field_name("name").is_some()
{
return None;
}
let mut candidates = analyzer
.declarations(file)
.into_iter()
.filter(|candidate| {
candidate.is_class()
&& analyzer.ranges(candidate).into_iter().any(|range| {
range.start_byte == node.start_byte() && range.end_byte == node.end_byte()
})
})
.collect::<Vec<_>>();
candidates.sort_by_key(|candidate| candidate.fq_name());
candidates.dedup();
match candidates.as_slice() {
[candidate] => Some(candidate.clone()),
_ => None,
}
}
pub fn function_local_type_resolution(
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
node: Node<'_>,
components: &[String],
source: &str,
) -> Option<LexicalTypeResolution> {
let (head, rest) = components.split_first()?;
if is_c_source_file(file) {
return None;
}
let declaration = function_local_class_declaration(node, head, source)?;
let Some(mut unit) = function_local_class_unit(analyzer, file, declaration, head, source)
else {
return Some(LexicalTypeResolution::Missing);
};
for component in rest {
let mut nested = analyzer
.declarations(file)
.into_iter()
.filter(|candidate| {
candidate.is_class()
&& candidate.identifier() == component
&& brokk_bifrost_core::analyzer::default_parent_fq_name(candidate)
.is_some_and(|parent| parent == unit.fq_name())
})
.collect::<Vec<_>>();
nested.dedup();
match nested.as_slice() {
[child] => unit = child.clone(),
[] => return Some(LexicalTypeResolution::Missing),
_ => return Some(LexicalTypeResolution::Ambiguous),
}
}
Some(LexicalTypeResolution::Resolved {
components: canonical_cpp_scope_components(&unit),
candidates: vec![unit.clone()],
unit,
})
}
pub fn is_function_local_unit(unit: &CodeUnit) -> bool {
let interner = segment_interner();
unit.fq().segments().split_last().is_some_and(|(_, scope)| {
scope
.iter()
.any(|&segment| interner.resolve(segment).1 == SegmentKind::Member)
})
}
fn function_local_class_declaration<'tree>(
node: Node<'tree>,
name: &str,
source: &str,
) -> Option<Node<'tree>> {
let mut path = Vec::new();
let mut child = node;
while let Some(ancestor) = child.parent() {
if ancestor.kind() == "compound_statement"
&& ancestor.parent().is_some_and(|function| {
function.kind() == "function_definition"
&& (is_recovered_declaration_scope_container(function, source)
|| crate::graph::extractor::is_malformed_wrapper_function_definition(
function,
))
})
{
break;
}
path.push((ancestor, child));
child = ancestor;
}
let outermost_block = path
.iter()
.rposition(|(ancestor, _)| ancestor.kind() == "compound_statement")?;
let mut in_member_body = false;
for &(ancestor, _) in &path[..=outermost_block] {
match ancestor.kind() {
"compound_statement" => {
if let Some(found) =
scope_local_class_declaration(ancestor, name, source, Some(node.start_byte()))
{
return Some(found);
}
}
"function_definition" | "lambda_expression" => in_member_body = true,
"field_declaration_list" => {
let class = ancestor.parent()?;
if local_class_name_is(class, name, source) {
return Some(class);
}
let before = (!in_member_body).then_some(node.start_byte());
if let Some(found) = scope_local_class_declaration(ancestor, name, source, before) {
return Some(found);
}
in_member_body = false;
}
_ => {}
}
}
None
}
fn scope_local_class_declaration<'tree>(
scope: Node<'tree>,
name: &str,
source: &str,
before: Option<usize>,
) -> Option<Node<'tree>> {
named_children_iter(scope)
.filter_map(scope_level_class_specifier)
.filter(|specifier| before.is_none_or(|before| specifier.end_byte() <= before))
.filter(|specifier| local_class_name_is(*specifier, name, source))
.last()
}
fn scope_level_class_specifier(statement: Node<'_>) -> Option<Node<'_>> {
let is_class_like = |node: &Node<'_>| {
matches!(
node.kind(),
"class_specifier" | "struct_specifier" | "union_specifier"
)
};
if is_class_like(&statement) {
return Some(statement);
}
if !matches!(
statement.kind(),
"declaration" | "field_declaration" | "type_definition"
) {
return None;
}
let specifier = statement
.child_by_field_name("type")
.filter(is_class_like)?;
(specifier.child_by_field_name("body").is_some()
|| statement.child_by_field_name("declarator").is_none())
.then_some(specifier)
}
fn local_class_name_is(specifier: Node<'_>, name: &str, source: &str) -> bool {
specifier
.child_by_field_name("name")
.is_some_and(|name_node| {
name_node.kind() == "type_identifier" && node_text(name_node, source) == name
})
}
fn function_local_class_unit(
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
declaration: Node<'_>,
name: &str,
source: &str,
) -> Option<CodeUnit> {
let definition = if declaration.child_by_field_name("body").is_some() {
declaration
} else {
let mut scope = declaration.parent()?;
if scope.kind() != "compound_statement" && scope.kind() != "field_declaration_list" {
scope = scope.parent()?;
}
named_children_iter(scope)
.filter_map(scope_level_class_specifier)
.filter(|specifier| specifier.child_by_field_name("body").is_some())
.find(|specifier| local_class_name_is(*specifier, name, source))?
};
let mut candidates = analyzer
.declarations(file)
.into_iter()
.filter(|candidate| {
candidate.is_class()
&& candidate.identifier() == name
&& is_function_local_unit(candidate)
&& analyzer.ranges(candidate).into_iter().any(|range| {
range.start_byte == definition.start_byte()
&& range.end_byte == definition.end_byte()
})
})
.collect::<Vec<_>>();
candidates.dedup();
match candidates.as_slice() {
[candidate] => Some(candidate.clone()),
_ => None,
}
}
fn logical_type_candidate(candidates: Vec<&CodeUnit>) -> Result<CodeUnit, TypeCandidateFailure> {
let Some(first) = candidates.first() else {
return Err(TypeCandidateFailure::Unresolvable);
};
if candidates
.iter()
.all(|candidate| candidate.kind() == first.kind() && candidate.fq_name() == first.fq_name())
{
Ok((*first).clone())
} else {
Err(TypeCandidateFailure::Ambiguous)
}
}
fn unique_logical_type_candidate(candidates: Vec<&CodeUnit>) -> Option<CodeUnit> {
logical_type_candidate(candidates).ok()
}
fn unique_type_candidate_preserving_alias(
analyzer: &CppGraphSource<'_>,
file: &ProjectFile,
candidates: &[&CodeUnit],
) -> Option<CodeUnit> {
let first = *candidates.first()?;
if declared_type_alias(analyzer, first) {
return candidates
.iter()
.all(|candidate| {
declared_type_alias(analyzer, candidate)
&& candidate.kind() == first.kind()
&& candidate.fq_name() == first.fq_name()
&& candidate.source() == first.source()
})
.then(|| first.clone());
}
if analyzer.reference_uses_c_semantics(file)
&& first.is_class()
&& indexed_c_tag_kind(analyzer, first).is_some()
{
let mut full_source = None;
let mut tag_kind = None;
for candidate in candidates.iter().copied() {
let candidate_tag_kind = indexed_c_tag_kind(analyzer, candidate)?;
if tag_kind
.replace(candidate_tag_kind)
.is_some_and(|existing| existing != candidate_tag_kind)
{
return None;
}
if cpp_class_declaration_strength(analyzer, candidate)
== CppClassDeclarationStrength::Full
&& full_source
.replace(candidate.source())
.is_some_and(|existing| existing != candidate.source())
{
return None;
}
}
}
candidates
.iter()
.all(|candidate| {
!declared_type_alias(analyzer, candidate)
&& candidate.kind() == first.kind()
&& candidate.fq_name() == first.fq_name()
})
.then(|| first.clone())
}
pub(crate) fn declared_type_alias(analyzer: &CppGraphSource<'_>, unit: &CodeUnit) -> bool {
brokk_bifrost_core::analyzer::capabilities::provider_declares_type_alias(
analyzer.type_alias_provider(),
unit,
)
}
pub fn field_declared_type_binding(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
visible_from: &ProjectFile,
field: &CodeUnit,
) -> Option<(String, Option<CodeUnit>, i32)> {
let fact = visibility.field_declared_type_fact(analyzer, field)?;
let normalized = normalize_field_type_text(&fact.type_text);
let primary = visibility.resolve_unique_canonical_type_for_declaration(
analyzer,
visible_from,
field,
&normalized,
);
let resolved = match (primary, fact.template_arguments.as_deref()) {
(Some(primary), Some(arguments)) => visibility
.resolve_template_arguments(visible_from, primary, arguments)
.ok(),
(resolved, None) => resolved,
(None, Some(_)) => None,
};
Some((normalized, resolved, fact.indirection))
}
fn decode_field_declared_type_fact(
analyzer: &CppGraphSource<'_>,
field: &CodeUnit,
) -> Option<DeclaredFieldTypeFact> {
if let Some(facts) = analyzer.declaration_source_properties(field)
&& let Some(first) = facts.first().and_then(|fact| fact.field_type.as_ref())
&& facts
.iter()
.all(|fact| fact.field_type.as_ref() == Some(first))
{
return Some(first.clone());
}
let Some(declaration) = analyzer.get_source(field, false) else {
return decode_indexed_field_declared_type_fact(analyzer, field);
};
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.ok()?;
let contextual = parse_sentinel(
"struct __bifrost_field_context { ",
SentinelInput::region(&declaration),
" };",
);
let (contextual_declaration, contextual_tree) = (contextual.source, contextual.tree);
let mut stack = vec![contextual_tree.root_node()];
while let Some(node) = stack.pop() {
if let Some(recovered) = recovered_pyobject_head_field(node, &contextual_declaration)
&& node_text(recovered.name, &contextual_declaration) == field.identifier()
{
return Some(DeclaredFieldTypeFact {
type_text: node_text(recovered.type_node, &contextual_declaration).to_string(),
indirection: recovered.pointer_depth(),
binds_indirectly: recovered.pointer_depth() > 0,
template_arguments: None,
});
}
if let Some(recovered) =
recovered_function_like_field_declarator(node, &contextual_declaration)
&& node_text(recovered.name, &contextual_declaration) == field.identifier()
{
let type_node = node
.child_by_field_name("type")
.or_else(|| first_type_child(node))?;
return Some(DeclaredFieldTypeFact {
type_text: node_text(type_node, &contextual_declaration).to_string(),
indirection: recovered.pointer_depth(),
binds_indirectly: true,
template_arguments: cpp_template_reference_arguments(
type_node,
&contextual_declaration,
),
});
}
if let Some(fact) =
decode_declared_field_type_node(node, field.identifier(), &contextual_declaration)
{
return Some(fact);
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
let tree = parser.parse(&declaration, None)?;
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
if let Some(fact) = decode_declared_field_type_node(node, field.identifier(), &declaration)
{
return Some(fact);
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
None
}
fn decode_indexed_field_declared_type_fact(
analyzer: &CppGraphSource<'_>,
field: &CodeUnit,
) -> Option<DeclaredFieldTypeFact> {
let cpp = analyzer.cpp?;
let prepared = cpp.prepared_syntax(analyzer.token, field.source())?;
let source = prepared.source();
let root = prepared.tree().root_node();
for range in analyzer.ranges(field) {
let end = range.start_byte.saturating_add(1).min(source.len());
let mut current = root.descendant_for_byte_range(range.start_byte, end);
while let Some(node) = current {
if matches!(node.kind(), "declaration" | "field_declaration")
&& let Some(fact) =
decode_declared_field_type_node(node, field.identifier(), source)
{
return Some(fact);
}
current = node.parent();
}
}
None
}
fn decode_declared_field_type_node(
node: Node<'_>,
field_name: &str,
source: &str,
) -> Option<DeclaredFieldTypeFact> {
if !matches!(node.kind(), "declaration" | "field_declaration") {
return None;
}
let type_node = node
.child_by_field_name("type")
.or_else(|| first_type_child(node))?;
cpp_declared_field_type_fact(node, type_node, field_name, source)
}
pub(crate) fn cpp_declared_field_type_fact(
node: Node<'_>,
type_node: Node<'_>,
field_name: &str,
source: &str,
) -> Option<DeclaredFieldTypeFact> {
let binding = declared_name_binding(node, type_node, field_name, source)?;
let declared_type = if matches!(
type_node.kind(),
"class_specifier" | "struct_specifier" | "union_specifier"
) {
type_node.child_by_field_name("name")
} else {
Some(type_node)
};
Some(DeclaredFieldTypeFact {
type_text: declared_type.map_or_else(
|| field_name.to_string(),
|declared_type| node_text(declared_type, source).to_string(),
),
indirection: binding.pointer_depth,
binds_indirectly: binding.indirect,
template_arguments: declared_type
.and_then(|declared_type| cpp_template_reference_arguments(declared_type, source)),
})
}
pub(crate) fn declarator_names_function_type(declarator: Node<'_>) -> bool {
let mut current = Some(declarator);
while let Some(node) = current {
match node.kind() {
"function_declarator" | "abstract_function_declarator" => return true,
"parenthesized_declarator" | "abstract_parenthesized_declarator" => {
current = node.named_child(0);
}
_ => current = node.child_by_field_name("declarator"),
}
}
false
}
fn decode_structured_alias_target(
analyzer: &CppGraphSource<'_>,
unit: &CodeUnit,
) -> Option<StructuredAliasTarget> {
let facts = analyzer.declaration_source_properties(unit)?;
let first = facts.first()?.alias_target.as_ref()?;
facts
.iter()
.all(|fact| fact.alias_target.as_ref() == Some(first))
.then(|| first.clone())
}
pub(crate) fn structured_alias_type_target(
mut type_node: Node<'_>,
source: &str,
) -> Option<StructuredAliasTarget> {
while type_node.kind() == "type_descriptor" {
type_node = type_node.child_by_field_name("type")?;
}
if type_node.kind() == "primitive_type" {
return Some(StructuredAliasTarget::Builtin);
}
if matches!(
type_node.kind(),
"class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
) {
type_node = type_node.child_by_field_name("name")?;
}
let global = type_node.child_by_field_name("scope").is_none()
&& type_node.child(0).is_some_and(|child| child.kind() == "::");
let mut components = Vec::new();
append_structured_type_components(type_node, source, &mut components)?;
let arguments = cpp_template_reference_arguments(type_node, source);
(!components.is_empty()).then_some(StructuredAliasTarget::Named {
components,
global,
arguments,
})
}
fn append_structured_type_components(
node: Node<'_>,
source: &str,
out: &mut Vec<String>,
) -> Option<()> {
match node.kind() {
"identifier" | "namespace_identifier" | "type_identifier" => {
out.push(node_text(node, source).to_string());
Some(())
}
"template_type" | "template_function" => {
append_structured_type_components(node.child_by_field_name("name")?, source, out)
}
"qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
let (scope, name) = qualified_name_parts(node)?;
if let Some(scope) = scope {
append_structured_type_components(scope, source, out)?;
}
append_structured_type_components(name, source, out)
}
_ => None,
}
}
pub(crate) struct DeclaredNameBinding {
pub pointer_depth: i32,
pub indirect: bool,
}
pub(crate) fn declared_name_binding(
declaration: Node<'_>,
type_node: Node<'_>,
field_name: &str,
source: &str,
) -> Option<DeclaredNameBinding> {
let mut stack = Vec::new();
let mut cursor = declaration.walk();
stack.extend(
declaration
.named_children(&mut cursor)
.filter(|child| !same_node(*child, type_node)),
);
while let Some(node) = stack.pop() {
if matches!(node.kind(), "identifier" | "field_identifier")
&& node_text(node, source) == field_name
{
let mut binding = DeclaredNameBinding {
pointer_depth: 0,
indirect: false,
};
let mut current = node.parent();
while let Some(parent) = current {
if same_node(parent, declaration) {
return Some(binding);
}
match parent.kind() {
"pointer_declarator" => {
binding.pointer_depth += 1;
binding.indirect = true;
}
"reference_declarator" => binding.indirect = true,
_ => {}
}
current = parent.parent();
}
return None;
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
None
}
fn normalize_field_type_text(type_text: &str) -> String {
const FIELD_SPECIFIERS: [&str; 8] = [
"extern ",
"static ",
"mutable ",
"constexpr ",
"constinit ",
"inline ",
"volatile ",
"const ",
];
let mut normalized = normalize_type_text(type_text);
loop {
let Some(stripped) = FIELD_SPECIFIERS
.iter()
.find_map(|specifier| normalized.strip_prefix(specifier))
else {
return normalized;
};
normalized = normalize_type_text(stripped);
}
}
pub fn declaration_mentions_type(node: Node<'_>, ctx: &ScanCtx<'_>, owner: &CodeUnit) -> bool {
let Some(type_node) = node.child_by_field_name("type") else {
return false;
};
ctx.visibility.resolves_to_type(
&ctx.analyzer,
ctx.file,
node_text(type_node, ctx.source),
owner,
)
}
pub fn declaration_is_object_construction_candidate(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
!ctx.analyzer
.declarations(ctx.file)
.into_iter()
.filter(|unit| unit.is_function())
.any(|unit| {
ctx.analyzer.ranges(&unit).iter().any(|range| {
node.start_byte() <= range.start_byte && range.end_byte <= node.end_byte()
})
})
}
pub enum DeclarationConstructorInitializer<'tree> {
Arguments(Node<'tree>),
Expression(Node<'tree>),
Empty,
}
pub fn declaration_constructor_initializer(
node: Node<'_>,
) -> DeclarationConstructorInitializer<'_> {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() == "init_declarator" {
let Some(value) = child
.child_by_field_name("value")
.or_else(|| first_named_child_of_kind(child, "initializer_list"))
.or_else(|| first_named_child_of_kind(child, "compound_literal_expression"))
else {
return DeclarationConstructorInitializer::Empty;
};
return match value.kind() {
"argument_list" | "initializer_list" => {
DeclarationConstructorInitializer::Arguments(value)
}
"compound_literal_expression" => call_arguments_node(value)
.map_or(DeclarationConstructorInitializer::Empty, |arguments| {
DeclarationConstructorInitializer::Arguments(arguments)
}),
_ => DeclarationConstructorInitializer::Expression(value),
};
}
if let Some(declarator) = declaration_declarator(node, child) {
return declarator_parameters(declarator)
.map_or(DeclarationConstructorInitializer::Empty, |parameters| {
DeclarationConstructorInitializer::Arguments(parameters)
});
}
}
DeclarationConstructorInitializer::Empty
}
pub fn declaration_constructor_arity(node: Node<'_>, _ctx: &ScanCtx<'_>) -> usize {
match declaration_constructor_initializer(node) {
DeclarationConstructorInitializer::Arguments(arguments) => {
argument_children(arguments).count()
}
DeclarationConstructorInitializer::Expression(_) => 1,
DeclarationConstructorInitializer::Empty => 0,
}
}
fn declarator_parameters(node: Node<'_>) -> Option<Node<'_>> {
let mut current = node;
loop {
if let Some(parameters) = current.child_by_field_name("parameters") {
return Some(parameters);
}
current = current.child_by_field_name("declarator")?;
}
}
fn first_descendant_of_kind<'tree>(root: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
brokk_bifrost_core::analyzer::tree_walk::first_named_descendant_of_kind_inclusive(root, kind)
}
fn argument_shape_may_change_arity(node: Node<'_>) -> bool {
if node.kind() == "identifier" {
return true;
}
if node.kind() == "parenthesized_expression" {
return false;
}
if node.kind() == "call_expression" {
return node
.child_by_field_name("function")
.is_some_and(|function| function.kind() == "identifier");
}
let mut stack = vec![node];
while let Some(descendant) = stack.pop() {
if descendant != node && descendant.kind() == "parenthesized_expression" {
continue;
}
if descendant.kind() == "identifier" {
return true;
}
if descendant.kind() == "call_expression" {
if descendant
.child_by_field_name("function")
.is_some_and(|function| function.kind() == "identifier")
{
return true;
}
continue;
}
push_named_children_reversed(descendant, &mut stack);
}
false
}
fn macro_expansion_shape_is_safe(
node: Node<'_>,
source: &str,
parameters: &[String],
environment: &MacroEnvironment,
) -> bool {
if matches!(node.kind(), "identifier" | "parenthesized_expression") {
return true;
}
if node.kind() == "call_expression" {
let Some(function) = node.child_by_field_name("function") else {
return true;
};
if function.kind() != "identifier" {
return true;
}
let function_name = node_text(function, source);
if parameters
.iter()
.any(|parameter| parameter == function_name)
{
return false;
}
if !environment.may_bind(function_name) {
return true;
}
let Some(arguments) = node.child_by_field_name("arguments") else {
return false;
};
return argument_children(arguments).all(|argument| {
if argument.kind() == "identifier"
&& parameters
.iter()
.any(|parameter| parameter == node_text(argument, source))
{
return false;
}
macro_expansion_shape_is_safe(argument, source, parameters, environment)
});
}
let mut stack = vec![node];
while let Some(descendant) = stack.pop() {
if descendant != node {
if descendant.kind() == "parenthesized_expression" {
continue;
}
if descendant.kind() == "call_expression" {
let expands = descendant
.child_by_field_name("function")
.filter(|function| function.kind() == "identifier")
.is_some_and(|function| environment.may_bind(node_text(function, source)));
if expands {
return false;
}
continue;
}
}
if descendant.kind() == "identifier" {
let identifier = node_text(descendant, source);
if parameters.iter().any(|parameter| parameter == identifier)
|| environment.may_bind(identifier)
{
return false;
}
}
push_named_children_reversed(descendant, &mut stack);
}
true
}
fn collect_structured_include_facts(prepared: &PreparedSyntaxTree) -> Arc<[StructuredIncludeFact]> {
let source = prepared.source();
let mut facts = Vec::new();
let mut nodes = vec![prepared.tree().root_node()];
while let Some(node) = nodes.pop() {
if node.kind() == "preproc_include" {
let Some(path) = node
.child_by_field_name("path")
.and_then(|path| crate::imports::include_path_literal_value(path, source))
.map(str::to_owned)
else {
continue;
};
facts.push(StructuredIncludeFact {
start_byte: node.start_byte(),
end_byte: node.end_byte(),
path,
});
continue;
}
push_named_children_reversed(node, &mut nodes);
}
Arc::from(facts.into_boxed_slice())
}
fn has_unresolved_include_visible_before_in_prepared(
file: &ProjectFile,
prepared: &PreparedSyntaxTree,
include_targets: &IncludeTargetIndex,
facts: &[StructuredIncludeFact],
before_byte: usize,
effective_guards: &ReferenceGuardAugmenter<'_>,
) -> bool {
let guards = OnceCell::new();
let reference = CallableReferenceContext {
effective_guards,
file_guards: OnceCell::new(),
file,
position: Some(CallableReferencePosition {
prepared,
byte: before_byte,
guards: &guards,
}),
};
let root = prepared.tree().root_node();
facts
.iter()
.filter(|fact| fact.end_byte <= before_byte)
.any(|fact| {
let node = root
.descendant_for_byte_range(fact.start_byte, fact.end_byte)
.expect("structured include fact range must be in prepared tree");
assert_eq!(
node.kind(),
"preproc_include",
"structured include fact range must identify its include node"
);
callable_preprocessor_context_is_visible_for_reference(
node,
prepared.source(),
&reference,
) && resolve_include_targets_with_index(file, &fact.path, include_targets).is_empty()
})
}
fn has_preprocessor_conditional_ancestor(mut node: Node<'_>, source: &str) -> bool {
let descendant = node;
while let Some(parent) = node.parent() {
if is_cpp_preprocessor_conditional(parent)
&& !is_file_covering_include_guard(parent, source)
&& preprocessor_conditional_contains_descendant(parent, descendant)
{
return true;
}
node = parent;
}
false
}
fn owning_preprocessor_conditionals(
root: Node<'_>,
event: Node<'_>,
source: &str,
) -> OwningPreprocessorConditionals {
if !has_preprocessor_conditional_ancestor(event, source) {
return OwningPreprocessorConditionals::default();
}
let start = event.start_byte();
let descendant = root
.descendant_for_byte_range(start, start.saturating_add(1).min(source.len()))
.expect("a byte inside the parsed tree names a descendant");
let mut owners = Vec::new();
let mut current = descendant.parent();
while let Some(conditional) = current {
if is_cpp_preprocessor_conditional(conditional)
&& !is_file_covering_include_guard(conditional, source)
&& preprocessor_conditional_contains_descendant(conditional, descendant)
{
owners.push(conditional.start_byte());
}
current = conditional.parent();
}
owners.into_boxed_slice()
}
fn is_file_covering_include_guard(node: Node<'_>, source: &str) -> bool {
node.parent()
.filter(|parent| parent.kind() == "translation_unit")
.is_some_and(|root| top_level_canonical_include_guard_name(root, source).is_some())
&& is_canonical_include_guard(node, source)
}
fn is_canonical_include_guard(node: Node<'_>, source: &str) -> bool {
if node.kind() != "preproc_ifdef"
|| node
.child(0)
.is_none_or(|directive| directive.kind() != "#ifndef")
|| node.child_by_field_name("alternative").is_some()
{
return false;
}
let Some(guard_name) = node.child_by_field_name("name") else {
return false;
};
let mut cursor = node.walk();
node.named_children(&mut cursor)
.find(|child| *child != guard_name && child.kind() != "comment")
.filter(|child| child.kind() == "preproc_def")
.and_then(|definition| definition.child_by_field_name("name"))
.is_some_and(|defined_name| {
node_text(defined_name, source) == node_text(guard_name, source)
})
}
fn top_level_canonical_include_guard_name(root: Node<'_>, source: &str) -> Option<String> {
let mut guard = None;
for child in named_children_iter(root) {
if child.kind() == "comment" || is_pragma_once(child, source) {
continue;
}
if guard.is_none() && is_canonical_include_guard(child, source) {
guard = Some(child);
} else {
return None;
}
}
guard
.and_then(|guard: Node<'_>| guard.child_by_field_name("name"))
.map(|name| node_text(name, source).to_string())
}
fn top_level_macro_include_protection(root: Node<'_>, source: &str) -> MacroIncludeProtection {
if (0..root.named_child_count())
.filter_map(|index| root.named_child(index))
.any(|child| is_pragma_once(child, source))
{
return MacroIncludeProtection::PragmaOnce;
}
top_level_canonical_include_guard_name(root, source)
.map(MacroIncludeProtection::MacroGuard)
.unwrap_or(MacroIncludeProtection::None)
}
fn is_pragma_once(node: Node<'_>, source: &str) -> bool {
node.kind() == "preproc_call"
&& node
.child_by_field_name("directive")
.is_some_and(|directive| node_text(directive, source) == "#pragma")
&& node
.child_by_field_name("argument")
.is_some_and(|argument| node_text(argument, source).trim() == "once")
}
fn parse_preproc_identifier(argument: &str) -> Option<String> {
let parsed = parse_sentinel(
"void __bifrost_undef() { ",
SentinelInput::replacement(argument),
"; }",
);
let (sentinel, tree) = (parsed.source, parsed.tree);
if tree.root_node().has_error() {
return None;
}
let statement = first_descendant_of_kind(tree.root_node(), "expression_statement")?;
let identifier = single_named_child_matching(statement, |child| !child.is_extra())?;
(identifier.kind() == "identifier").then(|| node_text(identifier, &sentinel).to_string())
}
pub fn extract_variable_name(node: Node<'_>, source: &str) -> Option<String> {
match node.kind() {
"identifier" | "field_identifier" => {
let name = node_text(node, source).trim();
(!name.is_empty()).then(|| name.to_string())
}
"abstract_array_declarator"
| "abstract_function_declarator"
| "abstract_parenthesized_declarator"
| "abstract_pointer_declarator"
| "abstract_reference_declarator" => None,
"function_declarator" => node
.child_by_field_name("declarator")
.or_else(|| node.child_by_field_name("name"))
.and_then(|child| extract_variable_name(child, source)),
_ => node
.child_by_field_name("declarator")
.or_else(|| node.child_by_field_name("name"))
.or_else(|| node.named_child(node.named_child_count().saturating_sub(1)))
.and_then(|child| extract_variable_name(child, source)),
}
}
pub fn is_c_source_file(file: &ProjectFile) -> bool {
LanguageDialect::for_path(Language::Cpp, file.rel_path()) == LanguageDialect::CppC
}
pub fn is_c_sizeof_expression_type_candidate(file: &ProjectFile, node: Node<'_>) -> bool {
if !is_c_source_file(file) || node.kind() != "identifier" {
return false;
}
let mut operand = node;
while let Some(parent) = operand.parent().filter(|parent| {
parent.kind() == "parenthesized_expression"
&& parent.named_child_count() == 1
&& parent.named_child(0) == Some(operand)
}) {
operand = parent;
}
operand.parent().is_some_and(|parent| {
parent.kind() == "sizeof_expression" && parent.child_by_field_name("value") == Some(operand)
})
}
pub fn c_offsetof_member_parts(node: Node<'_>) -> Option<(Node<'_>, Node<'_>)> {
if !is_c_offsetof_member_node(node) {
return None;
}
let expression = node.parent()?;
if expression.has_error() {
return None;
}
let closing = expression.child(expression.child_count().saturating_sub(1))?;
if closing.kind() != ")" || closing.is_missing() {
return None;
}
let type_descriptor = expression.child_by_field_name("type")?;
if type_descriptor.kind() != "type_descriptor"
|| type_descriptor.is_missing()
|| type_descriptor.has_error()
{
return None;
}
let type_specifier = type_descriptor.child_by_field_name("type")?;
if type_specifier.is_missing() || type_specifier.has_error() {
return None;
}
let type_reference = match type_specifier.kind() {
"class_specifier" | "struct_specifier" | "union_specifier" => {
type_specifier.child_by_field_name("name")?
}
_ => type_specifier,
};
(!type_reference.is_missing() && !type_reference.has_error()).then_some((type_reference, node))
}
pub fn is_c_offsetof_member_node(node: Node<'_>) -> bool {
node.kind() == "field_identifier"
&& node.parent().is_some_and(|parent| {
parent.kind() == "offsetof_expression"
&& parent.child_by_field_name("member") == Some(node)
})
}
pub fn cpp_template_function_has_type_owner(node: Node<'_>) -> bool {
if node.kind() != "template_function" {
return false;
}
let Some(arguments) = node
.parent()
.filter(|parent| parent.kind() == "template_argument_list")
else {
return false;
};
let Some(owner) = arguments.parent().filter(|parent| {
parent.kind() == "template_type"
&& parent.child_by_field_name("arguments") == Some(arguments)
}) else {
return false;
};
let mut current = owner;
while let Some(parent) = current.parent() {
if parent.kind() == "type_descriptor" && parent.child_by_field_name("type") == Some(current)
{
return true;
}
if parent.kind() == "qualified_identifier"
&& parent.child_by_field_name("name") == Some(current)
{
current = parent;
continue;
}
return false;
}
false
}
pub fn is_type_shaped_template_argument_name(node: Node<'_>) -> bool {
if node.kind() != "type_identifier" {
return false;
}
let Some(descriptor) = node
.parent()
.filter(|parent| parent.kind() == "type_descriptor")
else {
return false;
};
if descriptor.child_by_field_name("type") != Some(node) {
return false;
}
let Some(arguments) = descriptor
.parent()
.filter(|parent| parent.kind() == "template_argument_list")
else {
return false;
};
arguments.parent().is_some_and(|owner| {
matches!(
owner.kind(),
"template_type" | "template_function" | "template_method"
) && owner.child_by_field_name("arguments") == Some(arguments)
})
}
pub fn reference_uses_c_semantics(cpp: &dyn CppSource, file: &ProjectFile) -> bool {
is_c_source_file(file) || cpp.header_uses_c_semantics(file)
}
pub fn is_declarator_node(node: Node<'_>) -> bool {
matches!(
node.kind(),
"identifier"
| "field_identifier"
| "qualified_identifier"
| "scoped_identifier"
| "pointer_declarator"
| "reference_declarator"
| "array_declarator"
| "parenthesized_declarator"
| "function_declarator"
)
}
pub fn declaration_declarator<'tree>(
declaration: Node<'tree>,
child: Node<'tree>,
) -> Option<Node<'tree>> {
if !matches!(
declaration.kind(),
"declaration"
| "field_declaration"
| "parameter_declaration"
| "optional_parameter_declaration"
| "function_definition"
| "type_definition"
| "alias_declaration"
| "template_instantiation"
) {
return None;
}
if declaration
.child_by_field_name("type")
.is_some_and(|type_node| same_node(type_node, child))
{
return None;
}
if child.kind() == "init_declarator" {
return child.child_by_field_name("declarator");
}
let field = field_name_in_parent(declaration, child);
if (is_declarator_node(child) && matches!(field, Some("declarator") | None))
|| (declaration.kind() == "type_definition"
&& child.kind() == "type_identifier"
&& matches!(field, Some("declarator") | None))
{
Some(child)
} else {
None
}
}
pub fn cpp_binding_parts<'tree>(
node: Node<'tree>,
source: &str,
) -> Option<(String, Node<'tree>, Option<Node<'tree>>)> {
let declarator = node.child_by_field_name("declarator")?;
let name = extract_variable_name(declarator, source)?;
let type_node = node
.child_by_field_name("type")
.or_else(|| first_type_child(node));
Some((name, declarator, type_node))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RecoveredNamespaceRegion {
pub start: usize,
pub end: usize,
pub components: Vec<String>,
pub component_ranges: Vec<Range>,
pub component_name_ranges: Vec<Range>,
pub component_closes: Vec<Option<Range>>,
}
#[derive(Clone, Debug, Default)]
pub struct OrphanedNamespaceScopeIndex {
regions: Vec<RecoveredNamespaceRegion>,
brace_closes: HashMap<usize, Range>,
}
impl OrphanedNamespaceScopeIndex {
pub fn build(root: Node<'_>, source: &str) -> Self {
if !root.has_error() {
return Self::default();
}
#[derive(Clone, Copy)]
struct OpenHead {
head: Range,
open: usize,
}
struct PendingRegion {
start: usize,
end: usize,
components: Vec<String>,
heads: Vec<OpenHead>,
name_ranges: Vec<Range>,
}
struct Frame<'tree> {
node: Node<'tree>,
children: Vec<Node<'tree>>,
next: usize,
parsed_scope: Vec<String>,
run: Option<PendingRegion>,
}
fn frame<'tree>(
node: Node<'tree>,
mut parsed_scope: Vec<String>,
source: &str,
) -> Frame<'tree> {
if node.kind() == "namespace_definition"
&& let Some(name) = node.child_by_field_name("name")
{
let mut components = Vec::new();
if append_cpp_name_components(name, source, &mut components).is_some() {
parsed_scope.extend(components);
}
}
let mut cursor = node.walk();
Frame {
node,
children: node.children(&mut cursor).collect(),
next: 0,
parsed_scope,
run: None,
}
}
let mut regions = Vec::new();
let mut brace_closes = HashMap::default();
let mut namespace_brace_proofs = Vec::new();
let mut unpaired_closes: Vec<Range> = Vec::new();
let mut open = Vec::new();
let mut lexical_scope = Vec::new();
let mut lexical_heads: Vec<OpenHead> = Vec::new();
let mut lexical_name_ranges: Vec<Range> = Vec::new();
let mut frames = vec![frame(root, Vec::new(), source)];
while !frames.is_empty() {
let parent_node = frames.len().checked_sub(2).map(|index| frames[index].node);
let current = frames.last_mut().expect("frames is non-empty");
if current.next == current.children.len() {
regions.extend(frames.pop().expect("the frame just borrowed").run);
continue;
}
let child = current.children[current.next];
current.next += 1;
match child.kind() {
"{" if !child.is_missing() => {
regions.extend(current.run.take());
if let Some(namespace) = parent_node.filter(|parent| {
parent.kind() == "namespace_definition"
&& parent.child_by_field_name("body") == Some(current.node)
&& child.start_byte() == current.node.start_byte()
}) && let Some(proof) = namespace_body_brace_proof(current.node)
{
namespace_brace_proofs.push((child.start_byte(), namespace, proof));
}
let mut named = parent_node.and_then(|parent| {
namespace_body_name_components(parent, current.node, source)
});
if named.is_none() {
named = recovered_namespace_open_components(
¤t.children[..current.next - 1],
source,
);
}
open.push((child.start_byte(), lexical_scope.len()));
if let Some((written_at, components)) = named {
let head = OpenHead {
head: Range {
start_byte: written_at.start_byte(),
end_byte: child.end_byte(),
start_line: written_at.start_position().row + 1,
end_line: child.end_position().row + 1,
},
open: child.start_byte(),
};
lexical_heads.extend(std::iter::repeat_n(head, components.len()));
lexical_name_ranges
.extend(components.iter().map(|(_, name_range)| *name_range));
lexical_scope
.extend(components.into_iter().map(|(component, _)| component));
}
continue;
}
"}" if !child.is_missing() => {
regions.extend(current.run.take());
let close = Range {
start_byte: child.start_byte(),
end_byte: child.end_byte(),
start_line: child.start_position().row + 1,
end_line: child.end_position().row + 1,
};
match open.pop() {
Some((start, namespace_len)) => {
debug_assert!(
start < close.start_byte,
"a close pairs with an open before it: {start} then {close:?}"
);
lexical_scope.truncate(namespace_len);
lexical_heads.truncate(namespace_len);
lexical_name_ranges.truncate(namespace_len);
brace_closes.insert(start, close);
}
None => unpaired_closes.push(close),
}
continue;
}
_ => {}
}
if current.node.kind() != "namespace_definition"
&& lexical_scope != current.parsed_scope
{
match &mut current.run {
Some(run) if run.components == lexical_scope => run.end = child.end_byte(),
run => {
regions.extend(run.take());
*run = Some(PendingRegion {
start: child.start_byte(),
end: child.end_byte(),
components: lexical_scope.clone(),
heads: lexical_heads.clone(),
name_ranges: lexical_name_ranges.clone(),
});
}
}
} else {
regions.extend(current.run.take());
}
let descend = child.has_error()
|| (child.kind() == "function_definition"
&& child.child_by_field_name("body").is_some_and(|body| {
namespace_body_name_components(child, body, source).is_some()
}));
if descend {
let parsed_scope = current.parsed_scope.clone();
frames.push(frame(child, parsed_scope, source));
}
}
if !unpaired_closes.is_empty() {
brace_closes.clear();
} else {
let missing_namespaces = namespace_brace_proofs
.iter()
.filter_map(|(_, namespace, proof)| {
matches!(proof, NamespaceBodyBraceProof::Missing).then_some(namespace.id())
})
.collect::<HashSet<_>>();
for (open, namespace, proof) in namespace_brace_proofs {
if !std::iter::successors(Some(namespace), |node| node.parent())
.any(|ancestor| missing_namespaces.contains(&ancestor.id()))
{
continue;
}
match proof {
NamespaceBodyBraceProof::Real(close) => {
brace_closes.insert(open, close);
}
NamespaceBodyBraceProof::Missing => {
brace_closes.remove(&open);
}
}
}
}
let regions = regions
.into_iter()
.map(|region| {
debug_assert_eq!(
region.components.len(),
region.heads.len(),
"every recovered namespace level carries the head that writes it: {:?}",
region.components
);
debug_assert_eq!(
region.components.len(),
region.name_ranges.len(),
"every recovered namespace level carries the name token that writes it: {:?}",
region.components
);
let component_closes: Vec<_> = region
.heads
.iter()
.map(|level| brace_closes.get(&level.open).copied())
.collect();
let component_ranges = region
.heads
.iter()
.zip(&component_closes)
.map(|(level, close)| match close {
Some(close) => Range {
start_byte: level.head.start_byte,
end_byte: close.end_byte,
start_line: level.head.start_line,
end_line: close.end_line,
},
None => level.head,
})
.collect();
RecoveredNamespaceRegion {
start: region.start,
end: region.end,
components: region.components,
component_ranges,
component_name_ranges: region.name_ranges,
component_closes,
}
})
.collect();
Self {
regions,
brace_closes,
}
}
pub fn matching_close_brace(&self, open: usize) -> Option<Range> {
self.brace_closes.get(&open).copied()
}
pub fn is_empty(&self) -> bool {
self.regions.is_empty()
}
pub fn approximate_size(&self) -> usize {
self.regions.iter().fold(
self.brace_closes.len() * std::mem::size_of::<(usize, Range)>(),
|total, region| {
total
.saturating_add(std::mem::size_of::<RecoveredNamespaceRegion>())
.saturating_add(region.components.iter().map(String::len).sum::<usize>())
.saturating_add(region.component_ranges.len() * std::mem::size_of::<Range>())
.saturating_add(
region.component_name_ranges.len() * std::mem::size_of::<Range>(),
)
.saturating_add(
region.component_closes.len() * std::mem::size_of::<Option<Range>>(),
)
},
)
}
pub fn region_at(&self, byte: usize) -> Option<&RecoveredNamespaceRegion> {
self.regions
.iter()
.filter(|region| region.start <= byte && byte < region.end)
.min_by_key(|region| region.end - region.start)
}
pub fn enclosing_namespace_components(&self, node: Node<'_>, source: &str) -> Vec<String> {
self.enclosing_namespace_levels(node, source)
.into_iter()
.map(|(component, ..)| component)
.collect()
}
pub fn enclosing_namespace_levels(
&self,
node: Node<'_>,
source: &str,
) -> Vec<(String, Range, Range)> {
let mut parsed = Vec::new();
let mut current = node.parent();
while let Some(parent) = current {
if parent.kind() == "namespace_definition"
&& let Some(name) = parent.child_by_field_name("name")
&& let Some(components) = cpp_name_component_ranges(name, source)
{
let range = node_range(parent);
parsed.push((
parent.start_byte(),
components
.into_iter()
.map(|(component, name_range)| (component, range, name_range))
.collect(),
namespace_definition_has_real_braces(parent),
));
}
current = parent.parent();
}
parsed.reverse();
self.splice_recovered_path(parsed, node.start_byte(), |region, active_components| {
region
.components
.iter()
.take(active_components)
.cloned()
.zip(
region
.component_ranges
.iter()
.take(active_components)
.copied(),
)
.zip(
region
.component_name_ranges
.iter()
.take(active_components)
.copied(),
)
.map(|((component, range), name_range)| (component, range, name_range))
.collect()
})
}
pub fn restore_enclosing_namespaces(
&self,
parsed: Vec<(usize, Vec<String>, bool)>,
node_start: usize,
) -> Vec<String> {
self.splice_recovered_path(parsed, node_start, |region, active_components| {
region
.components
.iter()
.take(active_components)
.cloned()
.collect()
})
}
fn splice_recovered_path<T>(
&self,
parsed: Vec<(usize, Vec<T>, bool)>,
node_start: usize,
recovered: impl FnOnce(&RecoveredNamespaceRegion, usize) -> Vec<T>,
) -> Vec<T> {
let Some(region) = self.region_at(node_start) else {
return parsed
.into_iter()
.flat_map(|(_, levels, _)| levels)
.collect();
};
debug_assert_eq!(
region.components.len(),
region.component_closes.len(),
"every recovered namespace component carries its close proof: {region:?}"
);
debug_assert_eq!(
region.components.len(),
region.component_name_ranges.len(),
"every recovered namespace component carries its name token: {region:?}"
);
if region.components.is_empty() {
return parsed
.into_iter()
.filter(|(_, _, complete)| *complete)
.flat_map(|(_, levels, _)| levels)
.collect();
}
let active_components = region
.component_closes
.iter()
.take_while(|close| {
close
.as_ref()
.is_none_or(|close| node_start < close.end_byte)
})
.count();
recovered(region, active_components)
.into_iter()
.chain(
parsed
.into_iter()
.filter(|(start, _, _)| *start >= region.start)
.flat_map(|(_, levels, _)| levels),
)
.collect()
}
}
pub(super) fn namespace_definition_has_real_braces(namespace: Node<'_>) -> bool {
debug_assert_eq!(namespace.kind(), "namespace_definition");
let Some(body) = namespace.child_by_field_name("body") else {
return false;
};
let mut opening = false;
let mut closing = false;
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
match child.kind() {
"{" if !child.is_missing() => opening = true,
"}" if !child.is_missing() => closing = true,
_ => {}
}
}
opening && closing
}
fn recovered_namespace_open_components<'tree>(
preceding: &[Node<'tree>],
source: &str,
) -> Option<(Node<'tree>, Vec<(String, Range)>)> {
let mut head = Vec::new();
for &sibling in preceding.iter().rev() {
if sibling.kind() != "comment" {
head.push(sibling);
if head.len() == 2 {
break;
}
}
}
let [name, keyword] = head[..] else {
return None;
};
if keyword.kind() != "namespace" {
return None;
}
let components = cpp_name_component_ranges(name, source)?;
(!components.is_empty()).then_some((keyword, components))
}
fn namespace_body_name_components<'tree>(
parent: Node<'tree>,
body: Node<'tree>,
source: &str,
) -> Option<(Node<'tree>, Vec<(String, Range)>)> {
if parent.child_by_field_name("body") != Some(body) {
return None;
}
let (written_at, name) = match (parent.kind(), body.kind()) {
("namespace_definition", "declaration_list") => {
(parent, parent.child_by_field_name("name")?)
}
("function_definition", "compound_statement") => {
recovered_literal_namespace_head(parent, body, source)
.or_else(|| macro_modifier_namespace_head(parent, body, source))?
}
_ => return None,
};
let components = cpp_name_component_ranges(name, source)?;
(!components.is_empty()).then_some((written_at, components))
}
fn recovered_literal_namespace_head<'tree>(
parent: Node<'tree>,
body: Node<'tree>,
source: &str,
) -> Option<(Node<'tree>, Node<'tree>)> {
let mut cursor = parent.walk();
let children = parent
.children(&mut cursor)
.filter(|child| child.kind() != "comment")
.collect::<Vec<_>>();
let [prefix, keyword, error, actual_body] = children.as_slice() else {
return None;
};
if *actual_body != body
|| parent.child_by_field_name("type") != Some(*prefix)
|| parent.child_by_field_name("declarator") != Some(*keyword)
|| prefix.kind() != "type_identifier"
|| prefix.is_missing()
|| prefix.has_error()
|| prefix.child_count() != 0
|| keyword.kind() != "identifier"
|| keyword.is_missing()
|| keyword.has_error()
|| keyword.child_count() != 0
|| node_text(*keyword, source) != "namespace"
|| error.kind() != "ERROR"
|| error.is_missing()
|| body.is_missing()
{
return None;
}
let mut cursor = error.walk();
let names = error
.children(&mut cursor)
.filter(|child| child.kind() != "comment")
.collect::<Vec<_>>();
let [name] = names.as_slice() else {
return None;
};
if !matches!(name.kind(), "identifier" | "namespace_identifier")
|| name.is_missing()
|| name.has_error()
|| name.child_count() != 0
{
return None;
}
let open = body.child(0)?;
if open.kind() != "{"
|| open.is_missing()
|| open.has_error()
|| open.start_byte() != body.start_byte()
{
return None;
}
Some((*keyword, *name))
}
fn macro_modifier_namespace_head<'tree>(
parent: Node<'tree>,
body: Node<'tree>,
source: &str,
) -> Option<(Node<'tree>, Node<'tree>)> {
let mut cursor = parent.walk();
let children = parent
.children(&mut cursor)
.filter(|child| child.kind() != "comment")
.collect::<Vec<_>>();
let [prefix, keyword, name, actual_body] = children.as_slice() else {
return None;
};
if *actual_body != body
|| parent.child_by_field_name("type") != Some(*keyword)
|| parent.child_by_field_name("declarator") != Some(*name)
|| prefix.kind() != "macro_modifier"
|| prefix.is_missing()
|| prefix.has_error()
|| macro_modifier_identifier(*prefix).is_none()
|| keyword.kind() != "type_identifier"
|| keyword.is_missing()
|| keyword.has_error()
|| keyword.child_count() != 0
|| node_text(*keyword, source) != "namespace"
|| !matches!(name.kind(), "identifier" | "namespace_identifier")
|| name.is_missing()
|| name.has_error()
|| name.child_count() != 0
|| body.is_missing()
{
return None;
}
let open = body.child(0)?;
if open.kind() != "{"
|| open.is_missing()
|| open.has_error()
|| open.start_byte() != body.start_byte()
{
return None;
}
Some((*keyword, *name))
}
#[derive(Clone, Copy)]
enum NamespaceBodyBraceProof {
Real(Range),
Missing,
}
fn namespace_body_brace_proof(body: Node<'_>) -> Option<NamespaceBodyBraceProof> {
if body.kind() != "declaration_list" {
return None;
}
let mut cursor = body.walk();
let children = body.children(&mut cursor).collect::<Vec<_>>();
let close_index = children
.iter()
.rposition(|child| child.kind() == "}" && child.end_byte() == body.end_byte())?;
let close = children[close_index];
if close.is_missing() {
return Some(NamespaceBodyBraceProof::Missing);
}
let namespace_terminator_sibling = body
.parent()
.filter(|namespace| namespace.kind() == "namespace_definition")
.and_then(|namespace| namespace.next_sibling())
.is_some_and(|sibling| {
sibling.kind() == ";"
|| (sibling.kind() == "expression_statement"
&& sibling.child_count() == 1
&& sibling.child(0).is_some_and(|child| child.kind() == ";"))
});
if namespace_terminator_sibling
|| children
.get(close_index + 1)
.is_some_and(|sibling| sibling.kind() == ";")
|| children[..close_index]
.iter()
.rev()
.find(|child| !child.is_extra() && child.kind() != "{")
.is_some_and(|previous| {
matches!(
previous.kind(),
"expression_statement" | "labeled_statement"
)
})
{
return None;
}
Some(NamespaceBodyBraceProof::Real(node_range(close)))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RecoveredDeclaratorTypeContext {
Declaration,
FunctionDefinition,
Parameter,
}
pub fn recovered_macro_decorated_declarator_type(
node: Node<'_>,
) -> Option<RecoveredDeclaratorTypeContext> {
recovered_macro_decorated_type_node(node).map(|(_, context)| context)
}
pub fn recovered_macro_decorated_type_node(
node: Node<'_>,
) -> Option<(Node<'_>, RecoveredDeclaratorTypeContext)> {
if !matches!(node.kind(), "namespace_identifier" | "template_type") || node.is_missing() {
return None;
}
let qualified = node.parent()?;
if qualified.kind() != "qualified_identifier"
|| qualified.child_by_field_name("scope") != Some(node)
|| !(0..qualified.child_count())
.filter_map(|index| qualified.child(index))
.any(|child| child.kind() == "::" && child.is_missing())
{
return None;
}
if !concrete_recovered_declarator_name(qualified.child_by_field_name("name")?) {
return None;
}
let (declaration, context) = recovered_declarator_container(qualified)?;
let type_node = declaration
.child_by_field_name("type")
.filter(|type_node| {
*type_node != qualified
&& !type_node.is_missing()
&& type_node.start_byte() != type_node.end_byte()
})?;
Some((type_node, context))
}
pub fn macro_modifier_decorated_type_node(node: Node<'_>) -> bool {
matches!(node.kind(), "type_identifier" | "template_type")
&& node.parent().is_some_and(|declaration| {
matches!(
declaration.kind(),
"declaration"
| "function_definition"
| "parameter_declaration"
| "optional_parameter_declaration"
) && declaration.child_by_field_name("type") == Some(node)
&& declaration.child_by_field_name("declarator").is_some()
&& crate::declarations::cpp_declaration_type_follows_macro_modifier(
declaration,
node,
)
})
}
fn recovered_declarator_container(
mut declarator: Node<'_>,
) -> Option<(Node<'_>, RecoveredDeclaratorTypeContext)> {
loop {
let parent = declarator.parent()?;
if parent.kind() == "init_declarator" && has_field_child(parent, "declarator", declarator) {
return Some((
parent
.parent()
.filter(|declaration| declaration.kind() == "declaration")?,
RecoveredDeclaratorTypeContext::Declaration,
));
}
if parent.kind() == "declaration" && has_field_child(parent, "declarator", declarator) {
return Some((parent, RecoveredDeclaratorTypeContext::Declaration));
}
if parent.kind() == "function_definition"
&& has_field_child(parent, "declarator", declarator)
{
return Some((parent, RecoveredDeclaratorTypeContext::FunctionDefinition));
}
if matches!(
parent.kind(),
"parameter_declaration" | "optional_parameter_declaration"
) && has_field_child(parent, "declarator", declarator)
{
return Some((parent, RecoveredDeclaratorTypeContext::Parameter));
}
if !matches!(
parent.kind(),
"array_declarator"
| "function_declarator"
| "parenthesized_declarator"
| "pointer_declarator"
| "pointer_type_declarator"
| "reference_declarator"
) || !has_field_child(parent, "declarator", declarator)
{
return None;
}
declarator = parent;
}
}
fn has_field_child(parent: Node<'_>, field: &str, target: Node<'_>) -> bool {
brokk_bifrost_core::analyzer::structural::adapter_helpers::is_field_of(parent, target, field)
}
fn concrete_recovered_declarator_name(mut node: Node<'_>) -> bool {
loop {
if node.is_missing() || node.start_byte() == node.end_byte() {
return false;
}
match node.kind() {
"identifier" | "field_identifier" | "type_identifier" | "operator_name" => {
return true;
}
"array_declarator"
| "function_declarator"
| "parenthesized_declarator"
| "pointer_declarator"
| "pointer_type_declarator"
| "reference_declarator" => {
let Some(declarator) = node.child_by_field_name("declarator") else {
return false;
};
node = declarator;
}
_ => return false,
}
}
}
pub enum DesignatedInitializerOwner {
Resolved(CodeUnit),
Unresolved,
}
enum InitializerOwnerStep {
Field(String),
AggregateWrapper,
}
pub fn designated_initializer_owner(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
node: Node<'_>,
) -> Option<DesignatedInitializerOwner> {
if let Some(designator) = node
.parent()
.filter(|parent| parent.kind() == "field_designator")
{
let pair = designator.parent()?;
if pair.kind() != "initializer_pair" {
return None;
}
let mut cursor = pair.walk();
let designators = pair
.children_by_field_name("designator", &mut cursor)
.collect::<Vec<_>>();
let position = designators
.iter()
.position(|candidate| same_node(*candidate, designator))?;
let initializer = pair.parent()?;
if initializer.kind() != "initializer_list" {
return None;
}
let mut owner = initializer_list_owner(analyzer, visibility, file, source, initializer);
for prior in &designators[..position] {
let field = prior
.child_by_field_name("field")
.or_else(|| first_named_child_of_kind(*prior, "field_identifier"))?;
owner = owner.and_then(|owner| {
initializer_field_owner(analyzer, visibility, file, owner, node_text(field, source))
});
}
return Some(classified_designated_owner(owner));
}
let init_declarator = node.parent()?;
if init_declarator.child_by_field_name("declarator") != Some(node)
|| !crate::structural::is_recovered_designator_init_declarator(init_declarator)
{
return None;
}
Some(classified_designated_owner(declaration_owner(
analyzer,
visibility,
file,
source,
init_declarator.parent()?,
)))
}
fn classified_designated_owner(owner: Option<CodeUnit>) -> DesignatedInitializerOwner {
owner.map_or(
DesignatedInitializerOwner::Unresolved,
DesignatedInitializerOwner::Resolved,
)
}
fn initializer_list_owner(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
initializer: Node<'_>,
) -> Option<CodeUnit> {
let mut current = initializer;
let mut steps = Vec::new();
loop {
let parent = current.parent()?;
match parent.kind() {
"initializer_pair" if parent.child_by_field_name("value") == Some(current) => {
let designator = parent.child_by_field_name("designator")?;
let step = designator
.child_by_field_name("field")
.or_else(|| first_named_child_of_kind(designator, "field_identifier"))
.map(|field| InitializerOwnerStep::Field(node_text(field, source).to_string()))
.unwrap_or(InitializerOwnerStep::AggregateWrapper);
steps.push(step);
current = parent.parent()?;
}
"initializer_list" => {
current = parent;
}
"init_declarator" if parent.child_by_field_name("value") == Some(current) => {
let declaration = parent.parent()?;
let owner = declaration_owner(analyzer, visibility, file, source, declaration)?;
return apply_initializer_owner_steps(analyzer, visibility, file, owner, steps);
}
"compound_literal_expression"
if parent.child_by_field_name("value") == Some(current) =>
{
let type_node = parent.child_by_field_name("type")?;
let owner =
resolve_designated_owner_type(analyzer, visibility, file, source, type_node)?;
return apply_initializer_owner_steps(analyzer, visibility, file, owner, steps);
}
"ERROR" => current = parent,
_ => return None,
}
}
}
fn apply_initializer_owner_steps(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
mut owner: CodeUnit,
steps: Vec<InitializerOwnerStep>,
) -> Option<CodeUnit> {
for step in steps.into_iter().rev() {
if let InitializerOwnerStep::Field(field_name) = step {
owner = initializer_field_owner(analyzer, visibility, file, owner, &field_name)?;
}
}
Some(owner)
}
fn initializer_field_owner(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
owner: CodeUnit,
field_name: &str,
) -> Option<CodeUnit> {
let fields = visibility
.visible_members_for_owner_name(file, &owner, field_name)
.into_iter()
.filter(|field| field.is_field())
.collect::<Vec<_>>();
let field = match fields.as_slice() {
[field] => *field,
_ => return None,
};
field_declared_binding(analyzer, visibility, file, field)?.unit
}
fn declaration_owner(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
declaration: Node<'_>,
) -> Option<CodeUnit> {
if !matches!(declaration.kind(), "declaration" | "field_declaration") {
return None;
}
let type_node = declaration
.child_by_field_name("type")
.or_else(|| first_type_child(declaration))?;
resolve_designated_owner_type(analyzer, visibility, file, source, type_node)
}
fn resolve_designated_owner_type(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
type_node: Node<'_>,
) -> Option<CodeUnit> {
if let Some(owner) = anonymous_aggregate_owner(analyzer, file, type_node) {
return Some(owner);
}
if !is_globally_qualified_cpp_name(type_node)
&& let Some(components) = cpp_type_name_components(type_node, source)
&& let Some(resolution) =
function_local_type_resolution(analyzer, file, type_node, &components, source)
{
return match resolution {
LexicalTypeResolution::Resolved { unit, .. } => Some(unit).filter(CodeUnit::is_class),
LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => None,
};
}
let type_name = normalize_type_text(node_text(type_node, source));
visibility
.resolve_type(file, &type_name)
.filter(CodeUnit::is_class)
}
pub fn first_type_child(node: Node<'_>) -> Option<Node<'_>> {
let mut cursor = node.walk();
node.named_children(&mut cursor).find(|child| {
matches!(
child.kind(),
"type_identifier"
| "primitive_type"
| "qualified_identifier"
| "scoped_type_identifier"
| "struct_specifier"
| "union_specifier"
| "enum_specifier"
)
})
}
pub fn constructor_style_local_declaration<T: Clone + Eq + Hash>(
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
declarator: Node<'_>,
type_text: Option<&str>,
bindings: &LocalInferenceEngine<T>,
) -> bool {
if !has_ancestor_kind(declarator, "compound_statement") {
return false;
}
if declarator
.child_by_field_name("declarator")
.is_none_or(|declarator| declarator.kind() != "identifier")
{
return false;
}
if !type_text
.and_then(|text| visibility.resolve_type(file, text))
.is_some_and(|unit| unit.is_class())
{
return false;
}
declarator
.child_by_field_name("parameters")
.is_some_and(|parameters| {
constructor_parameters_look_like_expressions(parameters, source, bindings)
})
}
fn constructor_parameters_look_like_expressions<T: Clone + Eq + Hash>(
parameters: Node<'_>,
source: &str,
bindings: &LocalInferenceEngine<T>,
) -> bool {
let mut cursor = parameters.walk();
parameters.named_children(&mut cursor).any(|parameter| {
!matches!(
parameter.kind(),
"parameter_declaration" | "optional_parameter_declaration"
) || parameter_declaration_is_local_expression(parameter, source, bindings)
})
}
fn parameter_declaration_is_local_expression<T: Clone + Eq + Hash>(
parameter: Node<'_>,
source: &str,
bindings: &LocalInferenceEngine<T>,
) -> bool {
let text = node_text(parameter, source).trim();
if text
.chars()
.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
&& bindings.is_shadowed(text)
{
return true;
}
let Some(base) = parameter
.child_by_field_name("type")
.filter(|base| base.kind() == "type_identifier")
else {
return false;
};
let Some(subscript) = parameter
.child_by_field_name("declarator")
.filter(|declarator| declarator.kind() == "abstract_array_declarator")
else {
return false;
};
subscript.child_by_field_name("size").is_some()
&& bindings.is_shadowed(node_text(base, source).trim())
}
pub fn is_declaration_name(node: Node<'_>) -> bool {
let Some(parent) = node.parent() else {
return false;
};
if parent
.child_by_field_name("name")
.is_some_and(|name| same_node(name, node))
{
if matches!(
parent.kind(),
"class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
) {
return cpp_tag_specifier_declares_name(parent);
}
if matches!(
parent.kind(),
"namespace_definition"
| "namespace_alias_definition"
| "alias_declaration"
| "enumerator"
) {
return true;
}
}
let mut current = Some(parent);
while let Some(ancestor) = current {
let type_definition = ancestor.kind() == "type_definition";
let mut child_cursor = ancestor.walk();
if ancestor.named_children(&mut child_cursor).any(|child| {
declaration_declarator(ancestor, child).is_some_and(|declarator| {
declarator_name_path_contains(declarator, node, type_definition)
})
}) {
return true;
}
if matches!(
ancestor.kind(),
"declaration"
| "field_declaration"
| "parameter_declaration"
| "optional_parameter_declaration"
| "function_definition"
| "type_definition"
| "alias_declaration"
| "template_instantiation"
| "class_specifier"
| "struct_specifier"
| "union_specifier"
| "enum_specifier"
) {
return false;
}
current = ancestor.parent();
}
false
}
pub fn is_recovered_qualified_friend_class_type_reference(node: Node<'_>, source: &str) -> bool {
if !matches!(
node.kind(),
"qualified_identifier" | "scoped_type_identifier"
) {
return false;
}
let Some(declaration) = node
.parent()
.filter(|parent| parent.kind() == "declaration")
else {
return false;
};
if declaration.child_by_field_name("declarator") != Some(node)
|| !declaration
.child_by_field_name("type")
.is_some_and(|friend| {
friend.kind() == "type_identifier" && node_text(friend, source) == "friend"
})
{
return false;
}
let mut cursor = declaration.walk();
let mut errors = declaration
.named_children(&mut cursor)
.filter(|child| child.kind() == "ERROR");
let Some(error) = errors.next() else {
return false;
};
errors.next().is_none()
&& error.named_child_count() == 1
&& error.named_child(0).is_some_and(|class| {
class.kind() == "identifier" && node_text(class, source) == "class"
})
}
pub fn is_ordinary_macro_reference_node(node: Node<'_>) -> bool {
if !matches!(node.kind(), "identifier" | "field_identifier") {
return false;
}
if let Some(parent) = node.parent() {
if parent.kind() == "call_expression"
&& parent.child_by_field_name("function") == Some(node)
{
return false;
}
if matches!(parent.kind(), "labeled_statement" | "goto_statement")
&& parent.child_by_field_name("label") == Some(node)
{
return false;
}
}
if is_declaration_name(node) {
return false;
}
let mut current = node.parent();
while let Some(ancestor) = current {
match ancestor.kind() {
"preproc_ifdef" | "preproc_ifndef" => {
if ancestor
.child_by_field_name("name")
.is_some_and(|name| node_range_contains(name, node))
{
return false;
}
}
"preproc_if" | "preproc_elif" => {
if ancestor
.child_by_field_name("condition")
.is_some_and(|condition| node_range_contains(condition, node))
{
return false;
}
}
"preproc_else" => {}
kind if kind.starts_with("preproc_") => return false,
_ => {}
}
if matches!(
ancestor.kind(),
"translation_unit" | "function_definition" | "compound_statement"
) {
break;
}
current = ancestor.parent();
}
true
}
fn recovered_c_reference_node(
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
node: Node<'_>,
source: &str,
) -> bool {
if node.start_byte() >= node.end_byte()
|| node.is_error()
|| node.is_missing()
|| !matches!(
node.kind(),
"identifier" | "field_identifier" | "type_identifier" | "namespace_identifier"
)
|| recovered_c_macro_binding_role(node)
|| recovered_c_label_role(node)
{
return false;
}
let name = node_text(node, source);
let recovered_function_call = recovered_c_function_call(visibility, file, node, name);
let recovered_macro_call = recovered_c_function_declarator_invocation(node)
&& visibility.macro_name_may_be_bound_at(file, name, node.start_byte());
let recovered_parenthesized_reference = recovered_c_parenthesized_declarator_reference(node);
if is_declaration_name(node)
&& !recovered_c_explicit_assignment_callee(visibility, file, node, name)
&& !recovered_function_call
&& !recovered_macro_call
&& !recovered_parenthesized_reference
{
return false;
}
if !name.is_empty() && visibility.macro_name_may_be_bound_at(file, name, node.start_byte()) {
return true;
}
if recovered_c_explicit_assignment_callee(visibility, file, node, name) {
return true;
}
if recovered_parenthesized_reference {
return true;
}
if matches!(node.kind(), "type_identifier" | "namespace_identifier") {
if recovered_function_call {
return true;
}
return visibility
.visible_identifier_candidates(file, name)
.any(|candidate| candidate.is_class() || candidate.is_module());
}
let visible = visibility
.visible_identifier_candidates(file, name)
.next()
.is_some();
visible
&& (recovered_c_reference_anchor(node)
|| recovered_c_error_expression_leaf(node)
|| recovered_function_call)
}
fn push_recovered_c_range(
ranges: &mut Vec<Range>,
seen: &mut HashSet<(usize, usize)>,
start_byte: usize,
end_byte: usize,
node: Node<'_>,
limit: usize,
) -> bool {
if start_byte >= end_byte || !seen.insert((start_byte, end_byte)) {
return true;
}
if ranges.len() >= limit {
return false;
}
ranges.push(Range {
start_byte,
end_byte,
start_line: node.start_position().row,
end_line: node.end_position().row,
});
true
}
fn recovered_c_error_expression_leaf(node: Node<'_>) -> bool {
let mut current = node.parent();
while let Some(parent) = current {
if parent.is_error() {
let Some(anchor) = parent.parent() else {
return false;
};
return anchor.kind().ends_with("_expression")
|| matches!(
anchor.kind(),
"argument_list"
| "return_statement"
| "expression_statement"
| "case_statement"
| "initializer_list"
| "field_designator"
| "enumerator"
);
}
if matches!(
parent.kind(),
"translation_unit" | "function_definition" | "compound_statement"
) {
return false;
}
current = parent.parent();
}
false
}
fn recovered_c_function_call(
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
node: Node<'_>,
name: &str,
) -> bool {
if !matches!(
node.kind(),
"identifier" | "field_identifier" | "type_identifier"
) {
return false;
}
let error_call_prefix = node.parent().is_some_and(|error| {
error.is_error()
&& error
.parent()
.is_some_and(|parent| parent.kind() == "compound_statement")
}) && node
.prev_sibling()
.is_none_or(|previous| previous.kind() == ";")
&& node.next_sibling().is_some_and(|open| {
open.kind() == "("
&& open.next_named_sibling().is_some_and(|argument| {
argument.kind() == "parameter_declaration" && !argument.has_error()
})
});
(error_call_prefix || recovered_c_function_declarator_invocation(node))
&& visibility
.visible_identifier_candidates(file, name)
.any(CodeUnit::is_function)
}
pub fn recovered_c_block_call_envelope(declarator: Node<'_>) -> Option<Node<'_>> {
if declarator.kind() != "function_declarator" || !declarator.has_error() {
return None;
}
let definition = declarator.parent().filter(|parent| {
parent.kind() == "function_definition"
&& parent.child_by_field_name("declarator") == Some(declarator)
})?;
if definition.child_by_field_name("type").is_some()
|| definition.named_child(0) != Some(declarator)
|| definition.child_by_field_name("body").is_none()
|| definition.parent()?.kind() != "compound_statement"
{
return None;
}
Some(definition)
}
fn recovered_c_function_declarator_invocation(node: Node<'_>) -> bool {
let mut current = if node.parent().is_some_and(|parent| {
parent.kind() == "macro_type_specifier" && parent.child_by_field_name("name") == Some(node)
}) {
node.parent().expect("checked macro_type_specifier parent")
} else {
let mut function_declarator = if node.parent().is_some_and(|parent| {
parent.kind() == "function_declarator"
&& parent.child_by_field_name("declarator") == Some(node)
}) {
node.parent().expect("checked function declarator parent")
} else {
let Some(parameter) = node.parent().filter(|parent| {
parent.kind() == "parameter_declaration"
&& parent.child_by_field_name("type") == Some(node)
}) else {
return false;
};
if !parameter
.child_by_field_name("declarator")
.is_some_and(|declarator| declarator.kind() == "abstract_function_declarator")
{
return false;
}
let Some(parameters) = parameter
.parent()
.filter(|parent| parent.kind() == "parameter_list")
else {
return false;
};
let Some(function_declarator) = parameters
.parent()
.filter(|parent| parent.kind() == "function_declarator")
else {
return false;
};
function_declarator
};
while let Some(parent) = function_declarator.parent().filter(|parent| {
parent.kind() == "function_declarator"
&& parent.child_by_field_name("declarator") == Some(function_declarator)
}) {
function_declarator = parent;
}
let Some(current) = function_declarator.parent().filter(|parent| {
parent.is_error() || recovered_c_block_call_envelope(function_declarator).is_some()
}) else {
return false;
};
current
};
loop {
let Some(parent) = current.parent() else {
return false;
};
if matches!(
parent.kind(),
"translation_unit"
| "compound_statement"
| "preproc_if"
| "preproc_ifdef"
| "preproc_ifndef"
| "preproc_else"
| "preproc_elif"
) {
return true;
}
if parent.kind() == "function_definition"
&& parent.child_by_field_name("declarator") == Some(current)
&& parent.named_child(0) == Some(current)
&& parent.child_by_field_name("body").is_some()
{
return true;
}
if parent.is_error()
|| matches!(
parent.kind(),
"parameter_declaration"
| "parameter_list"
| "function_declarator"
| "abstract_function_declarator"
| "parenthesized_declarator"
)
{
current = parent;
continue;
}
return false;
}
}
fn recovered_c_parenthesized_declarator_reference(node: Node<'_>) -> bool {
if node
.parent()
.is_some_and(|parent| parent.kind() == "type_descriptor")
{
let Some(type_descriptor) = node.parent() else {
return false;
};
let Some(error) = type_descriptor.parent().filter(|parent| parent.is_error()) else {
return false;
};
if error.named_child_count() != 1 || error.named_child(0) != Some(type_descriptor) {
return false;
}
let Some(dependent) = error
.parent()
.and_then(|parent| parent.parent())
.filter(|parent| parent.kind() == "dependent_type")
else {
return false;
};
return dependent
.child(0)
.is_some_and(|keyword| keyword.kind() == "typename");
}
let Some(error) = node.parent().filter(|parent| parent.is_error()) else {
return false;
};
if error.named_child_count() != 1 || error.named_child(0) != Some(node) {
return false;
}
let Some(declarator) = error
.parent()
.filter(|parent| parent.kind() == "parenthesized_declarator")
else {
return false;
};
let Some(declaration) = declarator
.parent()
.filter(|parent| parent.kind() == "declaration")
else {
return false;
};
if declaration.child_by_field_name("declarator") != Some(declarator) {
return false;
}
let Some(type_node) = declaration.child_by_field_name("type") else {
return false;
};
type_node.kind() == "dependent_type"
&& type_node
.child(0)
.is_some_and(|keyword| keyword.kind() == "typename")
}
fn recovered_c_explicit_assignment_callee(
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
node: Node<'_>,
name: &str,
) -> bool {
let mut current = node;
let error = loop {
let Some(parent) = current.parent() else {
return false;
};
if parent.is_error() {
break parent;
}
current = parent;
};
let mut cursor = error.walk();
let explicit_recovery_precedes_callee = error
.named_children(&mut cursor)
.take_while(|child| child.start_byte() < node.start_byte())
.any(|child| child.kind() == "explicit_function_specifier");
if !explicit_recovery_precedes_callee {
return false;
}
visibility
.visible_identifier_candidates(file, name)
.any(CodeUnit::is_function)
}
fn recovered_c_macro_binding_role(mut node: Node<'_>) -> bool {
while let Some(parent) = node.parent() {
if matches!(
parent.kind(),
"preproc_def" | "preproc_function_def" | "preproc_params"
) {
return true;
}
if parent.is_error()
|| matches!(
parent.kind(),
"translation_unit" | "function_definition" | "compound_statement"
)
{
return false;
}
node = parent;
}
false
}
fn recovered_c_label_role(node: Node<'_>) -> bool {
node.parent().is_some_and(|parent| {
matches!(parent.kind(), "labeled_statement" | "goto_statement")
&& parent.child_by_field_name("label") == Some(node)
})
}
fn recovered_c_reference_anchor(mut node: Node<'_>) -> bool {
while let Some(parent) = node.parent() {
if parent.is_error() {
return false;
}
if parent.kind() == "optional_parameter_declaration"
&& parent
.child_by_field_name("default_value")
.is_some_and(|value| node_range_contains(value, node))
{
return true;
}
if parent.kind().ends_with("_expression")
|| matches!(
parent.kind(),
"argument_list"
| "return_statement"
| "expression_statement"
| "case_statement"
| "initializer_list"
| "init_declarator"
| "array_declarator"
| "field_designator"
| "enumerator"
)
{
return true;
}
if matches!(
parent.kind(),
"translation_unit"
| "function_definition"
| "compound_statement"
| "declaration"
| "field_declaration"
| "parameter_declaration"
) {
return false;
}
node = parent;
}
false
}
pub fn parameter_belongs_to_callable_scope(parameter: Node<'_>) -> bool {
let mut current = parameter.parent();
while let Some(ancestor) = current {
if ancestor.kind() == "lambda_expression" {
return ancestor
.child_by_field_name("declarator")
.is_some_and(|declarator| {
declarator.start_byte() <= parameter.start_byte()
&& parameter.end_byte() <= declarator.end_byte()
});
}
if ancestor.kind() == "function_definition" {
return ancestor
.child_by_field_name("declarator")
.is_some_and(|declarator| {
declarator.start_byte() <= parameter.start_byte()
&& parameter.end_byte() <= declarator.end_byte()
});
}
current = ancestor.parent();
}
false
}
pub fn is_parameter_type_reference(node: Node<'_>) -> bool {
let mut current = node.parent();
while let Some(ancestor) = current {
if matches!(
ancestor.kind(),
"parameter_declaration" | "optional_parameter_declaration"
) {
return crate::declarations::cpp_type_field_contains_node(ancestor, node);
}
if matches!(
ancestor.kind(),
"function_definition" | "lambda_expression" | "compound_statement"
) {
return false;
}
current = ancestor.parent();
}
false
}
pub fn cpp_tag_specifier_ancestors_before_type_reference_context(
specifier: Node<'_>,
) -> impl Iterator<Item = Node<'_>> {
std::iter::successors(specifier.parent(), |ancestor| ancestor.parent()).take_while(|ancestor| {
!matches!(
ancestor.kind(),
"type_descriptor"
| "parameter_declaration"
| "optional_parameter_declaration"
| "template_argument_list"
| "cast_expression"
)
})
}
pub fn cpp_tag_specifier_declares_name(specifier: Node<'_>) -> bool {
if specifier.child_by_field_name("body").is_some() {
return true;
}
for ancestor in cpp_tag_specifier_ancestors_before_type_reference_context(specifier) {
match ancestor.kind() {
"declaration" | "field_declaration" => {
let mut cursor = ancestor.walk();
return ancestor
.children_by_field_name("declarator", &mut cursor)
.next()
.is_none();
}
"translation_unit" => return true,
_ => {}
}
}
false
}
pub fn declarator_name_node(node: Node<'_>) -> Option<Node<'_>> {
match node.kind() {
"identifier"
| "field_identifier"
| "qualified_identifier"
| "scoped_identifier"
| "operator_name"
| "destructor_name"
| "literal_operator_name" => Some(node),
"reference_declarator" | "parenthesized_declarator" => {
node.named_child(0).and_then(declarator_name_node)
}
_ => node
.child_by_field_name("declarator")
.or_else(|| node.child_by_field_name("name"))
.or_else(|| node.child_by_field_name("field"))
.and_then(declarator_name_node),
}
}
pub fn declarator_path_contains_before_name(
declarator: Node<'_>,
name: Node<'_>,
candidate: Node<'_>,
) -> bool {
let mut current = Some(declarator);
while let Some(node) = current {
if same_node(node, candidate) {
return true;
}
if same_node(node, name) {
return false;
}
current = node
.child_by_field_name("declarator")
.or_else(|| node.child_by_field_name("name"))
.or_else(|| node.child_by_field_name("field"));
}
false
}
fn declarator_name_path_contains(
declarator: Node<'_>,
candidate: Node<'_>,
allow_type_identifier: bool,
) -> bool {
let Some(name) = declarator_name_leaf(declarator, allow_type_identifier) else {
return false;
};
declarator_path_contains_before_name(declarator, name, candidate)
}
fn declarator_name_leaf(node: Node<'_>, allow_type_identifier: bool) -> Option<Node<'_>> {
match node.kind() {
"identifier"
| "field_identifier"
| "operator_name"
| "destructor_name"
| "literal_operator_name" => Some(node),
"type_identifier" if allow_type_identifier => Some(node),
_ => node
.child_by_field_name("declarator")
.or_else(|| node.child_by_field_name("name"))
.or_else(|| node.child_by_field_name("field"))
.and_then(|child| declarator_name_leaf(child, allow_type_identifier)),
}
}
pub fn is_nested_type_node(node: Node<'_>) -> bool {
node.parent().is_some_and(|parent| {
matches!(
parent.kind(),
"qualified_identifier" | "scoped_type_identifier" | "template_type"
)
})
}
pub struct OutOfLineMemberDefinitionOwners<'tree> {
pub owners: Vec<(Node<'tree>, CodeUnit)>,
innermost: Option<(Node<'tree>, CodeUnit)>,
}
impl OutOfLineMemberDefinitionOwners<'_> {
pub fn innermost(&self) -> Option<(Node<'_>, &CodeUnit)> {
self.innermost.as_ref().map(|(node, owner)| (*node, owner))
}
}
pub struct QualifiedOwnerComponents<'tree> {
pub nodes: Vec<Node<'tree>>,
pub names: Vec<String>,
pub global: bool,
}
pub fn qualified_name_has_concrete_scope_separators(node: Node<'_>) -> bool {
let mut stack = vec![node];
let mut found_separator = false;
while let Some(current) = stack.pop() {
if !matches!(
current.kind(),
"qualified_identifier" | "scoped_identifier" | "scoped_type_identifier"
) {
continue;
}
let mut current_has_separator = false;
for child in children_iter(current) {
if child.kind() == "::" {
if child.is_missing() {
return false;
}
current_has_separator = true;
found_separator = true;
}
}
if !current_has_separator {
return false;
}
for field in ["scope", "name"] {
if let Some(child) = current.child_by_field_name(field)
&& matches!(
child.kind(),
"qualified_identifier" | "scoped_identifier" | "scoped_type_identifier"
)
{
stack.push(child);
}
}
}
found_separator
}
pub fn qualified_owner_components<'tree>(
node: Node<'tree>,
source: &str,
) -> Option<QualifiedOwnerComponents<'tree>> {
if !qualified_name_has_concrete_scope_separators(node) {
return None;
}
let mut nodes = cpp_name_component_nodes(node)?;
nodes.pop()?;
if nodes.is_empty() {
return None;
}
let names = nodes
.iter()
.map(|component| node_text(*component, source).to_string())
.collect();
Some(QualifiedOwnerComponents {
nodes,
names,
global: is_globally_qualified_cpp_name(node),
})
}
pub fn out_of_line_member_definition_owner<'tree>(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
node: Node<'tree>,
) -> Option<OutOfLineMemberDefinitionOwners<'tree>> {
if !matches!(node.kind(), "qualified_identifier" | "scoped_identifier")
|| !has_ancestor_kind(node, "function_definition")
|| !is_function_declarator_name_root(node)
{
return None;
}
let qualified = qualified_owner_components(node, source)?;
let lexical_scope = enclosing_namespace_components(node, source)?;
let mut owners = Vec::new();
let mut innermost = None;
for component_count in 1..=qualified.names.len() {
if let LexicalTypeResolution::Resolved { unit, .. } = visibility
.resolve_type_components_lexically(
analyzer,
file,
&qualified.names[..component_count],
qualified.global,
&lexical_scope,
)
&& !owners
.iter()
.any(|(_, existing)| same_visible_symbol(existing, &unit))
{
if component_count == qualified.names.len() {
innermost = Some((qualified.nodes[component_count - 1], unit.clone()));
}
owners.push((qualified.nodes[component_count - 1], unit));
}
}
if innermost.is_none() {
let indexed_owner_components = visibility
.indexed_enclosing_owner_scope(analyzer, file, node)
.or_else(|| {
if qualified.names.len() <= 1 {
return None;
}
let range = Range {
start_byte: node.start_byte(),
end_byte: node.end_byte(),
start_line: node.start_position().row,
end_line: node.end_position().row,
};
let start = analyzer.enclosing_code_unit(file, &range)?;
let mut components = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
&cpp_name_for(&start),
);
components.pop();
Some(components)
});
if let Some(indexed_owner_components) = indexed_owner_components
&& indexed_owner_components.len() > qualified.names.len()
&& indexed_owner_components.ends_with(&qualified.names)
&& indexed_namespace_path_is_recoverable(
&lexical_scope,
&indexed_owner_components,
qualified.names.len(),
)
&& (qualified.names.len() > 1 || !qualified.global)
{
let namespace_count = indexed_owner_components.len() - qualified.names.len();
for component_count in 1..=qualified.names.len() {
let expected = &indexed_owner_components[..namespace_count + component_count];
let owner_node = qualified.nodes[component_count - 1];
for owner in visibility
.visible_identifier_candidates(file, &qualified.names[component_count - 1])
.filter(|candidate| candidate.is_class())
.filter(|candidate| {
canonical_cpp_scope_components(candidate) == expected
&& visibility.external_type_candidate_visible_in_context(
analyzer, file, candidate, node,
)
})
{
if component_count == qualified.names.len() && innermost.is_none() {
innermost = Some((owner_node, owner.clone()));
}
if !owners
.iter()
.any(|(_, existing)| same_symbol(existing, owner))
{
owners.push((owner_node, owner.clone()));
}
}
}
}
}
(!owners.is_empty()).then_some(OutOfLineMemberDefinitionOwners { owners, innermost })
}
fn is_function_declarator_name_root(node: Node<'_>) -> bool {
let mut current = node;
while let Some(parent) = current.parent() {
if parent.kind() == "function_declarator" {
return parent.child_by_field_name("declarator") == Some(current);
}
if matches!(
parent.kind(),
"pointer_declarator" | "reference_declarator" | "parenthesized_declarator"
) && parent.child_by_field_name("declarator") == Some(current)
{
current = parent;
continue;
}
return false;
}
false
}
pub fn append_cpp_name_components(
node: Node<'_>,
source: &str,
out: &mut Vec<String>,
) -> Option<()> {
out.extend(
cpp_name_component_nodes(node)?
.into_iter()
.map(|component| node_text(component, source).to_string()),
);
Some(())
}
fn cpp_name_component_ranges(node: Node<'_>, source: &str) -> Option<Vec<(String, Range)>> {
cpp_name_component_nodes(node).map(|components| {
components
.into_iter()
.map(|component| {
(
node_text(component, source).to_string(),
node_range(component),
)
})
.collect()
})
}
pub fn cpp_type_name_components(node: Node<'_>, source: &str) -> Option<Vec<String>> {
let mut components = Vec::new();
append_cpp_name_components(node, source, &mut components)?;
Some(components)
}
pub fn unique_macro_replacement_type_candidate(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
components: &[String],
) -> Option<CodeUnit> {
let terminal = components.last()?;
let mut candidates = Vec::new();
for candidate in visibility
.visible_identifier_candidates(file, terminal)
.filter(|candidate| candidate.is_class() || declared_type_alias(analyzer, candidate))
.filter(|candidate| canonical_cpp_scope_components(candidate).ends_with(components))
{
if !candidates
.iter()
.any(|existing| same_logical_symbol(existing, candidate))
{
candidates.push(candidate.clone());
}
}
(candidates.len() == 1).then(|| candidates.remove(0))
}
pub fn cpp_qualified_name_has_scope_suffix(qualified: &str, scope: &str) -> bool {
qualified == scope
|| qualified
.strip_suffix(scope)
.is_some_and(|prefix| prefix.ends_with("::"))
}
pub fn is_cpp_template_argument_type_leaf(node: Node<'_>) -> bool {
let Some(type_descriptor) = node.parent() else {
return false;
};
if type_descriptor.kind() != "type_descriptor"
|| type_descriptor.child_by_field_name("type") != Some(node)
{
return false;
}
let Some(arguments) = type_descriptor.parent() else {
return false;
};
if arguments.kind() != "template_argument_list" {
return false;
}
arguments.parent().is_some_and(|parent| {
matches!(parent.kind(), "template_type" | "template_function")
&& parent.child_by_field_name("arguments") == Some(arguments)
})
}
pub fn cpp_template_reference_arguments(
mut node: Node<'_>,
source: &str,
) -> Option<Vec<CppTemplateExpression>> {
loop {
match node.kind() {
"template_type" | "template_function" => {
let arguments = node.child_by_field_name("arguments")?;
let mut cursor = arguments.walk();
return Some(
arguments
.named_children(&mut cursor)
.filter(|argument| !argument.is_extra() && argument.kind() != "comment")
.map(|argument| CppTemplateExpression {
text: normalize_cpp_whitespace(node_text(argument, source)),
term: cpp_template_term(
argument,
source,
&[],
&ParentIndex::unindexed(),
),
})
.collect(),
);
}
"qualified_identifier" | "scoped_type_identifier" | "type_descriptor" => {
node = node
.child_by_field_name("name")
.or_else(|| node.child_by_field_name("type"))?;
}
_ => return None,
}
}
}
fn cpp_reconcile_primary_template_parameters(
candidates: &[(&CodeUnit, &CppTemplateMetadata)],
preferred: &CodeUnit,
) -> Option<Vec<CppTemplateParameterMetadata>> {
let canonical = candidates
.iter()
.find_map(|(unit, metadata)| (*unit == preferred).then_some(*metadata))?;
let mut merged = canonical
.parameters
.iter()
.map(|parameter| CppTemplateParameterMetadata {
name: parameter.name.clone(),
kind: parameter.kind,
variadic: parameter.variadic,
default: None,
})
.collect::<Vec<_>>();
for (_, metadata) in candidates {
if metadata.parameters.len() != merged.len() {
return None;
}
let rename_bindings = metadata
.parameters
.iter()
.zip(&merged)
.map(|(parameter, canonical)| {
(
parameter.name.clone(),
CppTemplateTerm::Parameter(canonical.name.clone()),
)
})
.collect::<HashMap<_, _>>();
for ((parameter, canonical), merged_parameter) in metadata
.parameters
.iter()
.zip(&canonical.parameters)
.zip(&mut merged)
{
if parameter.kind != canonical.kind || parameter.variadic != canonical.variadic {
return None;
}
let Some(default) = ¶meter.default else {
continue;
};
let normalized_term = cpp_substitute_template_term(&default.term, &rename_bindings)?;
if let Some(existing) = &merged_parameter.default {
if !cpp_template_terms_equal(&existing.term, &normalized_term) {
return None;
}
} else {
merged_parameter.default = Some(CppTemplateExpression {
text: default.text.clone(),
term: normalized_term,
});
}
}
}
Some(merged)
}
pub fn cpp_bind_template_arguments(
parameters: &[CppTemplateParameterMetadata],
explicit_arguments: &[CppTemplateExpression],
) -> Option<(Vec<CppTemplateExpression>, HashMap<String, CppTemplateTerm>)> {
let variadic_index = parameters.iter().position(|parameter| parameter.variadic);
if variadic_index.is_some_and(|index| {
index + 1 != parameters.len()
|| parameters[index + 1..]
.iter()
.any(|parameter| parameter.variadic)
}) {
return None;
}
let fixed_count = variadic_index.unwrap_or(parameters.len());
if variadic_index.is_none() && explicit_arguments.len() > fixed_count {
return None;
}
let explicit_fixed_count = explicit_arguments.len().min(fixed_count);
let mut expanded = explicit_arguments[..explicit_fixed_count]
.iter()
.map(cpp_clone_template_expression_iterative)
.collect::<Vec<_>>();
let mut bindings = HashMap::default();
for (parameter, argument) in parameters[..explicit_fixed_count].iter().zip(&expanded) {
bindings.insert(
parameter.name.clone(),
cpp_clone_template_term_iterative(&argument.term),
);
}
for parameter in ¶meters[explicit_fixed_count..fixed_count] {
let default = parameter.default.as_ref()?;
let term = cpp_substitute_template_term(&default.term, &bindings)?;
bindings.insert(parameter.name.clone(), term.clone());
expanded.push(CppTemplateExpression {
text: default.text.clone(),
term,
});
}
if let Some(index) = variadic_index {
let packed_arguments = &explicit_arguments[explicit_fixed_count..];
expanded.extend(
packed_arguments
.iter()
.map(cpp_clone_template_expression_iterative),
);
bindings.insert(
parameters[index].name.clone(),
CppTemplateTerm::Node {
kind: "parameter_pack".to_string(),
children: packed_arguments
.iter()
.map(|argument| cpp_clone_template_term_iterative(&argument.term))
.collect(),
},
);
}
Some((expanded, bindings))
}
fn cpp_specialization_matches(
metadata: &CppTemplateMetadata,
arguments: &[CppTemplateExpression],
) -> bool {
cpp_specialization_terms_match(
&metadata.parameters,
&metadata.specialization_arguments,
arguments,
)
}
fn cpp_specialization_terms_match(
parameters: &[CppTemplateParameterMetadata],
patterns: &[CppTemplateExpression],
arguments: &[CppTemplateExpression],
) -> bool {
if patterns.len() != arguments.len() {
return false;
}
let parameter_names = parameters
.iter()
.map(|parameter| parameter.name.as_str())
.collect::<HashSet<_>>();
let mut bindings: HashMap<String, CppTemplateTerm> = HashMap::default();
patterns.iter().zip(arguments).all(|(pattern, argument)| {
cpp_unify_template_term(
&pattern.term,
&argument.term,
¶meter_names,
&mut bindings,
)
})
}
fn cpp_specialization_more_specialized(
candidate: &CppTemplateMetadata,
other: &CppTemplateMetadata,
) -> bool {
cpp_specialization_pattern_accepts(other, candidate)
&& !cpp_specialization_pattern_accepts(candidate, other)
}
fn cpp_specialization_pattern_accepts(
broader: &CppTemplateMetadata,
narrower: &CppTemplateMetadata,
) -> bool {
cpp_specialization_terms_match(
&broader.parameters,
&broader.specialization_arguments,
&narrower.specialization_arguments,
)
}
pub fn cpp_substitute_template_term(
term: &CppTemplateTerm,
bindings: &HashMap<String, CppTemplateTerm>,
) -> Option<CppTemplateTerm> {
enum Work<'a> {
Visit(&'a CppTemplateTerm),
Build { kind: String, child_count: usize },
}
let mut work = vec![Work::Visit(term)];
let mut substituted = Vec::new();
while let Some(next) = work.pop() {
match next {
Work::Visit(CppTemplateTerm::Parameter(name)) => {
substituted.push(cpp_clone_template_term_iterative(bindings.get(name)?));
}
Work::Visit(CppTemplateTerm::Atom { kind, text }) => {
substituted.push(CppTemplateTerm::Atom {
kind: kind.clone(),
text: text.clone(),
});
}
Work::Visit(CppTemplateTerm::Node { kind, children }) => {
work.push(Work::Build {
kind: kind.clone(),
child_count: children.len(),
});
work.extend(children.iter().rev().map(Work::Visit));
}
Work::Build { kind, child_count } => {
let children = substituted.split_off(substituted.len() - child_count);
substituted.push(CppTemplateTerm::Node { kind, children });
}
}
}
substituted.pop()
}
pub fn cpp_substitute_template_arguments(
arguments: &[CppTemplateExpression],
bindings: &HashMap<String, CppTemplateTerm>,
) -> Option<Vec<CppTemplateExpression>> {
let mut substituted = Vec::new();
for argument in arguments {
let CppTemplateTerm::Node { kind, children } = &argument.term else {
substituted.push(CppTemplateExpression {
text: argument.text.clone(),
term: cpp_substitute_template_term(&argument.term, bindings)?,
});
continue;
};
if kind != "parameter_pack_expansion" {
substituted.push(CppTemplateExpression {
text: argument.text.clone(),
term: cpp_substitute_template_term(&argument.term, bindings)?,
});
continue;
}
let [pattern, CppTemplateTerm::Atom { text: ellipsis, .. }] = children.as_slice() else {
return None;
};
if ellipsis != "..." {
return None;
}
let mut pack_names = Vec::new();
let mut work = vec![pattern];
while let Some(term) = work.pop() {
match term {
CppTemplateTerm::Parameter(name)
if matches!(
bindings.get(name),
Some(CppTemplateTerm::Node { kind, .. }) if kind == "parameter_pack"
) =>
{
if !pack_names.contains(name) {
pack_names.push(name.clone());
}
}
CppTemplateTerm::Node { children, .. } => work.extend(children),
CppTemplateTerm::Parameter(_) | CppTemplateTerm::Atom { .. } => {}
}
}
let first_pack = pack_names.first()?;
let CppTemplateTerm::Node {
children: first_elements,
..
} = bindings.get(first_pack)?
else {
return None;
};
let pack_len = first_elements.len();
for pack_name in &pack_names {
let CppTemplateTerm::Node { children, .. } = bindings.get(pack_name)? else {
return None;
};
if children.len() != pack_len {
return None;
}
}
for index in 0..pack_len {
let mut element_bindings = bindings.clone();
for pack_name in &pack_names {
let CppTemplateTerm::Node { children, .. } = bindings.get(pack_name)? else {
return None;
};
element_bindings.insert(
pack_name.clone(),
cpp_clone_template_term_iterative(&children[index]),
);
}
substituted.push(CppTemplateExpression {
text: argument.text.clone(),
term: cpp_substitute_template_term(pattern, &element_bindings)?,
});
}
}
Some(substituted)
}
fn cpp_clone_template_term_iterative(term: &CppTemplateTerm) -> CppTemplateTerm {
enum Work<'a> {
Visit(&'a CppTemplateTerm),
Build { kind: String, child_count: usize },
}
let mut work = vec![Work::Visit(term)];
let mut cloned = Vec::new();
while let Some(next) = work.pop() {
match next {
Work::Visit(CppTemplateTerm::Parameter(name)) => {
cloned.push(CppTemplateTerm::Parameter(name.clone()));
}
Work::Visit(CppTemplateTerm::Atom { kind, text }) => {
cloned.push(CppTemplateTerm::Atom {
kind: kind.clone(),
text: text.clone(),
});
}
Work::Visit(CppTemplateTerm::Node { kind, children }) => {
work.push(Work::Build {
kind: kind.clone(),
child_count: children.len(),
});
work.extend(children.iter().rev().map(Work::Visit));
}
Work::Build { kind, child_count } => {
let children = cloned.split_off(cloned.len() - child_count);
cloned.push(CppTemplateTerm::Node { kind, children });
}
}
}
cloned
.pop()
.expect("template term traversal emits one root")
}
fn cpp_clone_template_expression_iterative(
expression: &CppTemplateExpression,
) -> CppTemplateExpression {
CppTemplateExpression {
text: expression.text.clone(),
term: cpp_clone_template_term_iterative(&expression.term),
}
}
pub fn cpp_unify_template_term(
pattern: &CppTemplateTerm,
argument: &CppTemplateTerm,
parameters: &HashSet<&str>,
bindings: &mut HashMap<String, CppTemplateTerm>,
) -> bool {
let mut work = vec![(pattern, argument)];
while let Some((pattern, argument)) = work.pop() {
match pattern {
CppTemplateTerm::Parameter(name) if parameters.contains(name.as_str()) => {
if let Some(bound) = bindings.get(name) {
if !cpp_template_terms_equal(bound, argument) {
return false;
}
} else {
bindings.insert(name.clone(), cpp_clone_template_term_iterative(argument));
}
}
CppTemplateTerm::Atom {
kind: pattern_kind,
text: pattern_text,
} => {
if !matches!(
argument,
CppTemplateTerm::Atom { kind, text }
if kind == pattern_kind && text == pattern_text
) {
return false;
}
}
CppTemplateTerm::Node {
kind: pattern_kind,
children: pattern_children,
} => {
let CppTemplateTerm::Node { kind, children } = argument else {
return false;
};
if kind != pattern_kind || children.len() != pattern_children.len() {
return false;
}
work.extend(pattern_children.iter().zip(children).rev());
}
CppTemplateTerm::Parameter(_) => return false,
}
}
true
}
pub(crate) fn cpp_template_terms_equal(left: &CppTemplateTerm, right: &CppTemplateTerm) -> bool {
let mut work = vec![(left, right)];
while let Some((left, right)) = work.pop() {
match (left, right) {
(CppTemplateTerm::Parameter(left), CppTemplateTerm::Parameter(right)) => {
if left != right {
return false;
}
}
(
CppTemplateTerm::Atom {
kind: left_kind,
text: left_text,
},
CppTemplateTerm::Atom {
kind: right_kind,
text: right_text,
},
) => {
if left_kind != right_kind || left_text != right_text {
return false;
}
}
(
CppTemplateTerm::Node {
kind: left_kind,
children: left_children,
},
CppTemplateTerm::Node {
kind: right_kind,
children: right_children,
},
) => {
if left_kind != right_kind || left_children.len() != right_children.len() {
return false;
}
work.extend(left_children.iter().zip(right_children).rev());
}
_ => return false,
}
}
true
}
pub fn cpp_name_component_nodes(node: Node<'_>) -> Option<Vec<Node<'_>>> {
let mut components = Vec::new();
let mut stack = vec![node];
while let Some(current) = stack.pop() {
match current.kind() {
"identifier"
| "field_identifier"
| "namespace_identifier"
| "type_identifier"
| "operator_name"
| "destructor_name" => components.push(current),
"template_type" | "template_function" => {
stack.push(current.child_by_field_name("name")?);
}
"dependent_name" => stack.push(current.named_child(0)?),
"qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
stack.push(current.child_by_field_name("name")?);
if let Some(scope) = current.child_by_field_name("scope") {
stack.push(scope);
}
}
"nested_namespace_specifier" => {
for index in (0..current.named_child_count()).rev() {
stack.push(current.named_child(index)?);
}
}
_ => return None,
}
}
Some(components)
}
pub fn is_globally_qualified_cpp_name(node: Node<'_>) -> bool {
node.child_by_field_name("scope").is_none()
&& node.child(0).is_some_and(|child| child.kind() == "::")
}
fn enclosing_namespace_components(node: Node<'_>, source: &str) -> Option<Vec<String>> {
let mut namespaces = Vec::new();
for parent in enclosing_cpp_namespace_nodes(node) {
if let Some(name) = parent.child_by_field_name("name") {
let mut components = Vec::new();
append_cpp_name_components(name, source, &mut components)?;
namespaces.push(components);
}
}
Some(namespaces.into_iter().flatten().collect())
}
fn indexed_namespace_path_is_recoverable(
lexical_scope: &[String],
indexed_owner_scope: &[String],
explicit_owner_component_count: usize,
) -> bool {
if lexical_scope.is_empty() {
return explicit_owner_component_count > 1;
}
if lexical_scope.len() >= indexed_owner_scope.len() {
return false;
}
super::cpp_namespace_path_is_ordered_subsequence(lexical_scope, indexed_owner_scope)
}
pub use brokk_bifrost_core::analyzer::tree_walk::has_ancestor_kind;
pub(crate) fn initialized_type_declaration_with_cast(node: Node<'_>) -> bool {
let mut current = Some(node);
while let Some(candidate) = current {
if candidate.kind() == "declaration" {
if !crate::declarations::cpp_type_field_contains_node(candidate, node) {
return false;
}
let mut cursor = candidate.walk();
return candidate.named_children(&mut cursor).any(|child| {
child.kind() == "init_declarator"
&& child
.child_by_field_name("value")
.is_some_and(|value| value.kind() == "cast_expression")
});
}
current = candidate.parent();
}
false
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum QualifiedAliasReferenceKind {
Ordinary,
ConstructorWithExpressionArgument,
ExhaustiveTemplate,
}
pub(crate) fn qualified_alias_reference_preserves_target(
node: Node<'_>,
target: &CodeUnit,
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
file: &ProjectFile,
source: &str,
) -> Option<QualifiedAliasReferenceKind> {
if !matches!(
node.kind(),
"qualified_identifier" | "scoped_identifier" | "scoped_type_identifier"
) {
return None;
}
let components = cpp_type_name_components(node, source)?;
let name = components.last()?;
analyzer.type_alias_provider().and_then(|provider| {
visibility
.visible_identifier_candidates(file, name)
.find_map(|candidate| {
let proof = provider.is_type_alias(candidate)
&& canonical_cpp_scope_components(candidate) == components
&& visibility.external_type_candidate_visible_in_context(
analyzer, file, candidate, node,
)
&& match cpp_template_reference_arguments(node, source) {
Some(arguments) => visibility.template_alias_arguments_preserve_target(
analyzer, file, candidate, &arguments, target,
),
None => visibility.structured_alias_primary_preserves_target(
analyzer, file, candidate, target,
),
};
proof.then(|| {
if cpp_template_reference_arguments(node, source).is_some()
&& visibility.is_exhaustive_same_fqn_type_declaration_family(
analyzer, file, candidate,
)
{
QualifiedAliasReferenceKind::ExhaustiveTemplate
} else if qualified_alias_constructor_has_expression_argument(node)
|| qualified_alias_local_constructor_declaration(node)
{
QualifiedAliasReferenceKind::ConstructorWithExpressionArgument
} else {
QualifiedAliasReferenceKind::Ordinary
}
})
})
})
}
pub(crate) fn qualified_alias_reference_requires_terminal(
reference: Option<QualifiedAliasReferenceKind>,
) -> bool {
matches!(
reference,
Some(
QualifiedAliasReferenceKind::ConstructorWithExpressionArgument
| QualifiedAliasReferenceKind::ExhaustiveTemplate
)
)
}
fn qualified_alias_constructor_has_expression_argument(node: Node<'_>) -> bool {
let Some(declaration) = node.parent().filter(|parent| {
parent.kind() == "declaration" && parent.child_by_field_name("type") == Some(node)
}) else {
return false;
};
let mut cursor = declaration.walk();
declaration.named_children(&mut cursor).any(|child| {
child.kind() == "init_declarator"
&& child
.child_by_field_name("value")
.filter(|value| value.kind() == "argument_list")
.is_some_and(|arguments| {
let mut cursor = arguments.walk();
arguments.named_children(&mut cursor).any(|argument| {
let is_parameter = matches!(
argument.kind(),
"parameter_declaration" | "optional_parameter_declaration"
);
if is_parameter {
argument
.child_by_field_name("type")
.is_some_and(|type_node| {
type_node.kind() == "type_identifier"
&& argument.child_by_field_name("declarator").is_none()
})
} else {
!argument.kind().ends_with("_literal")
&& !matches!(argument.kind(), "true" | "false" | "nullptr")
}
})
})
})
}
fn qualified_alias_local_constructor_declaration(node: Node<'_>) -> bool {
let Some(declaration) = node.parent().filter(|parent| {
parent.kind() == "declaration" && parent.child_by_field_name("type") == Some(node)
}) else {
return false;
};
if declaration
.parent()
.is_none_or(|parent| parent.kind() != "compound_statement")
{
return false;
}
let mut cursor = declaration.walk();
declaration
.named_children(&mut cursor)
.any(|child| child.kind() == "function_declarator")
}
pub fn function_terminal_node(mut node: Node<'_>) -> Node<'_> {
loop {
let next = match node.kind() {
"qualified_identifier"
| "scoped_identifier"
| "template_method"
| "template_function"
| "template_type" => node.child_by_field_name("name"),
"field_expression" => node.child_by_field_name("field"),
_ => None,
};
let Some(next) = next else {
return node;
};
node = next;
}
}
#[derive(Clone, Copy)]
pub struct RecoveredRelationalTemplateMemberCall<'tree> {
pub receiver: Node<'tree>,
pub member: Node<'tree>,
pub arity: usize,
}
pub fn recovered_relational_template_member_call(
field: Node<'_>,
) -> Option<RecoveredRelationalTemplateMemberCall<'_>> {
if field.kind() != "field_expression" {
return None;
}
let receiver = field
.child_by_field_name("argument")
.or_else(|| field.child_by_field_name("object"))?;
let member = field.child_by_field_name("field")?;
let less = field.parent()?;
if less.kind() != "binary_expression"
|| less.child_by_field_name("left") != Some(field)
|| less
.child_by_field_name("operator")
.is_none_or(|operator| operator.kind() != "<")
|| less.child_by_field_name("right").is_none()
{
return None;
}
let greater = less.parent()?;
if greater.kind() != "binary_expression"
|| greater.child_by_field_name("left") != Some(less)
|| greater
.child_by_field_name("operator")
.is_none_or(|operator| operator.kind() != ">")
{
return None;
}
let arguments = greater.child_by_field_name("right")?;
if arguments.kind() != "parenthesized_expression" {
return None;
}
let arity = parenthesized_call_argument_arity(arguments)?;
Some(RecoveredRelationalTemplateMemberCall {
receiver,
member,
arity,
})
}
fn parenthesized_call_argument_arity(arguments: Node<'_>) -> Option<usize> {
let expression = arguments.named_child(0)?;
if expression.kind() != "comma_expression" {
return Some(1);
}
let mut arity = 0usize;
let mut stack = vec![expression];
while let Some(node) = stack.pop() {
if node.kind() == "comma_expression" {
stack.push(node.child_by_field_name("right")?);
stack.push(node.child_by_field_name("left")?);
} else {
arity += 1;
}
}
Some(arity)
}
pub fn is_call_callee_node(mut node: Node<'_>) -> bool {
while let Some(parent) = node.parent() {
match parent.kind() {
"call_expression" => {
return parent
.child_by_field_name("function")
.or_else(|| parent.named_child(0))
== Some(node);
}
"qualified_identifier"
| "scoped_identifier"
| "template_function"
| "template_type"
| "field_expression" => node = parent,
_ => return false,
}
}
false
}
pub fn type_reference_hit_node(mut node: Node<'_>) -> Node<'_> {
loop {
let Some(parent) = node.parent() else {
return node;
};
if parent.kind() == "template_type"
&& parent
.child_by_field_name("name")
.is_some_and(|name| same_node(name, node))
{
node = parent;
continue;
}
if matches!(
parent.kind(),
"qualified_identifier" | "scoped_identifier" | "scoped_type_identifier"
) && (parent
.child_by_field_name("scope")
.is_some_and(|scope| same_node(scope, node))
|| parent
.child_by_field_name("name")
.is_some_and(|name| same_node(name, node)))
&& parent.end_byte() == node.end_byte()
{
node = parent;
continue;
}
return node;
}
}
pub fn normalize_type_text(value: &str) -> String {
strip_tag_type_prefix(
normalize_cpp_whitespace(value)
.trim_start_matches("const ")
.trim_end_matches('*')
.trim_end_matches('&')
.trim(),
)
.to_string()
}
use crate::declarations::strip_tag_type_prefix;
pub fn normalize_reference_name(value: &str) -> Option<String> {
let normalized = normalize_cpp_reference_text(value);
(!normalized.is_empty()).then_some(normalized)
}
pub fn normalize_cpp_reference_text(value: &str) -> String {
let mut text = normalize_cpp_whitespace(value)
.trim_start_matches("new ")
.trim()
.to_string();
if let Some(index) = text.find(['(', '{']) {
text.truncate(index);
}
if let Some(index) = text.find('<') {
text.truncate(index);
}
let normalized = text
.trim()
.trim_start_matches("const ")
.trim_end_matches(|ch: char| ch == '*' || ch == '&' || ch.is_whitespace())
.trim_matches(':')
.trim();
strip_tag_type_prefix(normalized).to_string()
}
pub fn cpp_name_for(unit: &CodeUnit) -> String {
let short = unit.short_name().replace(['.', '$'], "::");
if unit.package_name().is_empty() {
short
} else {
format!("{}::{}", unit.package_name(), short)
}
}
fn canonical_cpp_name_from_fq(unit: &CodeUnit) -> Option<String> {
let fq = unit.fq();
if fq.is_empty() {
return None;
}
let interner = brokk_bifrost_core::analyzer::fq_name::segment_interner();
Some(
fq.segments()
.iter()
.map(|&segment| interner.resolve(segment).0)
.collect::<Vec<_>>()
.join("::"),
)
}
fn canonical_cpp_name_matches(unit: &CodeUnit, expected: &str) -> bool {
canonical_cpp_name_from_fq(unit).as_deref() == Some(expected)
|| unit.fq().is_empty() && cpp_name_for(unit) == expected
}
pub fn canonical_cpp_scope_components(unit: &CodeUnit) -> Vec<String> {
let fq = unit.fq();
if !fq.is_empty() {
let interner = brokk_bifrost_core::analyzer::fq_name::segment_interner();
let segments = fq.segments();
let scope = segments
.iter()
.enumerate()
.filter_map(|(index, &segment)| {
let (text, kind) = interner.resolve(segment);
match kind {
SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => true,
SegmentKind::Member => index + 1 < segments.len(),
SegmentKind::Path | SegmentKind::Companion | SegmentKind::Unknown => false,
}
.then(|| text.to_string())
})
.collect();
return scope;
}
brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
&cpp_name_for(unit),
)
}
pub fn terminal_name(value: &str) -> &str {
value
.rsplit("::")
.next()
.unwrap_or(value)
.rsplit(['.', '-', '>'])
.next()
.unwrap_or(value)
.trim()
}
pub fn name_matches_terminal(value: &str, expected: &str) -> bool {
terminal_name(&normalize_cpp_reference_text(value)) == expected
}
pub fn name_matches_callable(value: &str, expected: &str) -> bool {
name_matches_terminal(value, expected)
|| expected.starts_with("operator")
&& terminal_name(&normalize_cpp_reference_text(value)) == "operator"
}
pub fn name_mentions(value: &str, expected: &str) -> bool {
normalize_cpp_reference_text(value)
.split("::")
.any(|part| part == expected)
}
pub fn reference_matches_unit(reference: &str, unit: &CodeUnit) -> bool {
let cpp_name = cpp_name_for(unit);
if reference.contains("::") {
return reference == cpp_name;
}
reference == cpp_name
|| terminal_name(reference) == unit.identifier()
&& (unit.package_name().is_empty() || reference == unit.identifier())
}
pub fn matches_kind_for_lookup(unit: &CodeUnit, kind: TargetKind) -> bool {
match kind {
TargetKind::Type
| TargetKind::Constructor
| TargetKind::Method
| TargetKind::MemberField => true,
TargetKind::FreeFunction => unit.is_function(),
TargetKind::GlobalField => unit.is_field(),
TargetKind::Macro => unit.is_macro(),
}
}
fn alias_target_matches_target(alias: &CppAlias, target: &CodeUnit) -> bool {
let normalized = normalize_cpp_reference_text(alias.target.trim().trim_end_matches(';'));
let target_name = cpp_name_for(target);
if normalized.contains("::") {
return normalized == target_name;
}
if let Some(namespace) = alias.namespace.as_deref() {
return namespace_prefixes(namespace)
.into_iter()
.any(|prefix| format!("{prefix}::{normalized}") == target_name);
}
target.package_name().is_empty() && normalized == target.identifier()
}
pub fn cpp_function_return_type_text(
analyzer: &CppGraphSource<'_>,
function: &CodeUnit,
) -> Option<String> {
let metadata = analyzer.signature_metadata(function);
let first = metadata.first()?.return_type_text()?;
metadata
.iter()
.all(|metadata| metadata.return_type_text() == Some(first))
.then(|| first.to_owned())
}
pub fn cpp_namespace_for(unit: &CodeUnit) -> Option<String> {
cpp_name_for(unit).rsplit_once("::").map(|(namespace, _)| {
namespace
.strip_prefix("anonymous_namespace::")
.unwrap_or(namespace)
.to_string()
})
}
pub fn namespace_prefixes(namespace: &str) -> Vec<String> {
let mut parts = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
brokk_bifrost_core::analyzer::Language::Cpp,
namespace,
);
let mut prefixes = Vec::new();
while !parts.is_empty() {
prefixes.push(parts.join("::"));
parts.pop();
}
prefixes
}
fn nearest_namespace_candidates(
candidates: Vec<CodeUnit>,
normalized: &str,
lexical_namespace: Option<&str>,
) -> Vec<CodeUnit> {
if normalized.contains("::") {
return candidates;
}
if let Some(namespace) = lexical_namespace {
for prefix in namespace_prefixes(namespace) {
let scoped = candidates
.iter()
.filter(|function| cpp_namespace_for(function).as_deref() == Some(prefix.as_str()))
.cloned()
.collect::<Vec<_>>();
if !scoped.is_empty() {
return scoped;
}
}
}
candidates
.into_iter()
.filter(|function| cpp_namespace_for(function).is_none_or(|namespace| namespace.is_empty()))
.collect()
}
pub fn enclosing_namespace_context(node: Node<'_>, source: &str) -> Option<String> {
let mut namespaces = Vec::new();
for parent in enclosing_cpp_namespace_nodes(node) {
if let Some(name) = parent.child_by_field_name("name") {
let namespace = normalize_cpp_reference_text(node_text(name, source));
if !namespace.is_empty() {
namespaces.push(namespace);
}
}
}
if namespaces.is_empty() {
None
} else {
Some(namespaces.join("::"))
}
}
pub fn type_owner_of(analyzer: &CppGraphSource<'_>, code_unit: &CodeUnit) -> Option<CodeUnit> {
type_owner_resolution(analyzer, code_unit).map(|owner| owner.unit)
}
fn type_owner_resolution(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
) -> Option<ResolvedTypeOwner> {
precise_parent_resolution(analyzer, code_unit).filter(|owner| !owner.unit.is_module())
}
fn target_type_owner_resolution(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
) -> Option<ResolvedTypeOwner> {
match type_owner_resolution(analyzer, code_unit) {
Some(owner) if owner.unit.is_class() && !owner.is_forward_declaration => Some(owner),
Some(_) | None => target_forward_owner_resolution(analyzer, code_unit),
}
}
fn target_forward_owner_resolution(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
) -> Option<ResolvedTypeOwner> {
if !code_unit.is_function() {
return None;
}
let owner_name = code_unit.fq().parent().filter(|owner| !owner.is_empty())?;
let cpp = analyzer.cpp?;
let mut visible_files = HashSet::default();
collect_include_closure(
analyzer,
&cpp.include_target_index(),
code_unit.source(),
&mut visible_files,
None,
);
let candidates = analyzer.workspace_definitions().exact(&owner_name);
let visible_candidates = candidates
.iter()
.filter(|candidate| candidate.is_class() && visible_files.contains(candidate.source()))
.cloned()
.collect::<Vec<_>>();
match classify_direct_owner_candidates(analyzer, visible_candidates.into_iter()) {
DirectOwnerResolution::UniqueFull(unit) => {
return Some(ResolvedTypeOwner {
unit,
is_forward_declaration: false,
});
}
DirectOwnerResolution::ForwardsOnly(forwards) => {
return (forwards.len() == 1).then(|| ResolvedTypeOwner {
unit: forwards.into_iter().next().unwrap(),
is_forward_declaration: true,
});
}
DirectOwnerResolution::Ambiguous => return None,
DirectOwnerResolution::None => {}
}
let candidates = candidates
.into_iter()
.filter(|candidate| candidate.is_class())
.collect::<Vec<_>>();
let (unit, is_forward_declaration) =
match classify_direct_owner_candidates(analyzer, candidates.iter().cloned()) {
DirectOwnerResolution::UniqueFull(unit) => (unit, false),
DirectOwnerResolution::ForwardsOnly(forwards) => {
(unique_logical_forward_owner(forwards)?, true)
}
DirectOwnerResolution::None | DirectOwnerResolution::Ambiguous => return None,
};
Some(ResolvedTypeOwner {
unit,
is_forward_declaration,
})
}
pub fn precise_parent_of(
analyzer: &CppGraphSource<'_>,
visibility: &VisibilityIndex<'_>,
code_unit: &CodeUnit,
) -> Option<CodeUnit> {
visibility.cached_precise_parent_of(analyzer, code_unit)
}
fn precise_parent_resolution(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
) -> Option<ResolvedTypeOwner> {
#[cfg(any(test, feature = "test-support"))]
if let Some(cpp) = analyzer.cpp {
cpp.record_cpp_parent_resolution_for_test();
}
if let Some(unit) = exact_structural_type_parent(analyzer, code_unit) {
return Some(ResolvedTypeOwner {
unit,
is_forward_declaration: false,
});
}
let fallback = analyzer.parent_of(code_unit);
if !code_unit.owner_is_type_scope() {
return fallback.map(|unit| ResolvedTypeOwner {
unit,
is_forward_declaration: false,
});
}
let owner_fq = code_unit
.fq()
.parent()
.expect("a unit with an owner identifier has a structured parent");
let owner_candidates = analyzer.workspace_definitions().exact(&owner_fq);
match same_source_owner(analyzer, code_unit, &owner_candidates) {
DirectOwnerResolution::UniqueFull(owner) => {
return Some(ResolvedTypeOwner {
unit: owner,
is_forward_declaration: false,
});
}
DirectOwnerResolution::Ambiguous => return None,
DirectOwnerResolution::ForwardsOnly(_) | DirectOwnerResolution::None => {}
}
match directly_included_owner(analyzer, code_unit, &owner_candidates) {
DirectOwnerResolution::UniqueFull(owner) => Some(ResolvedTypeOwner {
unit: owner,
is_forward_declaration: false,
}),
DirectOwnerResolution::Ambiguous => None,
DirectOwnerResolution::ForwardsOnly(forwards) => {
match visible_full_cpp_owner(analyzer, code_unit, &owner_candidates) {
FullOwnerResolution::Unique(owner) => Some(ResolvedTypeOwner {
unit: owner,
is_forward_declaration: false,
}),
FullOwnerResolution::None => {
unique_logical_forward_owner(forwards).map(|unit| ResolvedTypeOwner {
unit,
is_forward_declaration: true,
})
}
FullOwnerResolution::Ambiguous => None,
}
}
DirectOwnerResolution::None => {
match visible_full_cpp_owner(analyzer, code_unit, &owner_candidates) {
FullOwnerResolution::Unique(owner) => Some(ResolvedTypeOwner {
unit: owner,
is_forward_declaration: false,
}),
FullOwnerResolution::Ambiguous => None,
FullOwnerResolution::None => fallback
.filter(|parent| {
parent.source() == code_unit.source()
&& parent.fq() == &owner_fq
&& (!parent.is_class()
|| cpp_class_declaration_strength(analyzer, parent)
== CppClassDeclarationStrength::Full)
})
.map(|unit| ResolvedTypeOwner {
unit,
is_forward_declaration: false,
}),
}
}
}
}
fn exact_structural_type_parent(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
) -> Option<CodeUnit> {
if !code_unit.is_function() && !code_unit.is_field() {
return None;
}
let encoded_owner = code_unit.short_name().rsplit_once('.')?.0; let cpp = analyzer.cpp?;
let parent = cpp.structural_parent_of(code_unit)?;
(!parent.is_module()
&& parent.source() == code_unit.source()
&& parent.package_name() == code_unit.package_name()
&& parent.short_name() == encoded_owner)
.then_some(parent)
}
fn same_source_owner(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
owner_candidates: &[CodeUnit],
) -> DirectOwnerResolution {
let candidates = owner_candidates
.iter()
.filter(|candidate| candidate.is_class() && candidate.source() == code_unit.source())
.cloned()
.collect::<Vec<_>>();
let candidates = prefer_member_declaring_owners(analyzer, code_unit, candidates);
classify_direct_owner_candidates(analyzer, candidates.into_iter())
}
fn visible_full_cpp_owner(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
owner_candidates: &[CodeUnit],
) -> FullOwnerResolution {
let Some(cpp) = analyzer.cpp else {
return FullOwnerResolution::None;
};
let mut visible_files = HashSet::default();
collect_include_closure(
analyzer,
&cpp.include_target_index(),
code_unit.source(),
&mut visible_files,
None,
);
let candidates = owner_candidates
.iter()
.filter(|candidate| candidate.is_class() && visible_files.contains(candidate.source()))
.cloned()
.collect::<Vec<_>>();
let candidates = prefer_member_declaring_owners(analyzer, code_unit, candidates);
match collapse_owner_candidates(candidates.into_iter().map(|candidate| {
let strength = cpp_class_declaration_strength(analyzer, &candidate);
(candidate, strength)
})) {
DirectOwnerResolution::UniqueFull(owner) => FullOwnerResolution::Unique(owner),
DirectOwnerResolution::ForwardsOnly(_) | DirectOwnerResolution::None => {
FullOwnerResolution::None
}
DirectOwnerResolution::Ambiguous => FullOwnerResolution::Ambiguous,
}
}
pub enum DirectOwnerResolution {
None,
ForwardsOnly(Vec<CodeUnit>),
UniqueFull(CodeUnit),
Ambiguous,
}
enum FullOwnerResolution {
None,
Unique(CodeUnit),
Ambiguous,
}
fn directly_included_owner(
analyzer: &CppGraphSource<'_>,
code_unit: &CodeUnit,
owner_candidates: &[CodeUnit],
) -> DirectOwnerResolution {
let Some(cpp) = analyzer.cpp else {
return DirectOwnerResolution::None;
};
let direct_includes: HashSet<ProjectFile> = analyzer
.canonical_include_paths(code_unit.source())
.unwrap_or_default()
.into_iter()
.flat_map(|include| {
resolve_include_targets_with_index(
code_unit.source(),
&include,
&cpp.include_target_index(),
)
})
.collect();
let candidates = owner_candidates
.iter()
.filter(|candidate| candidate.is_class() && direct_includes.contains(candidate.source()))
.cloned()
.collect::<Vec<_>>();
let candidates = prefer_member_declaring_owners(analyzer, code_unit, candidates);
classify_direct_owner_candidates(analyzer, candidates.into_iter())
}
fn prefer_member_declaring_owners(
analyzer: &CppGraphSource<'_>,
member: &CodeUnit,
candidates: Vec<CodeUnit>,
) -> Vec<CodeUnit> {
let matching = candidates
.iter()
.filter(|owner| owner_declares_member(analyzer, owner, member))
.cloned()
.collect::<Vec<_>>();
if matching.is_empty() {
candidates
} else {
matching
}
}
fn owner_declares_member(
analyzer: &CppGraphSource<'_>,
owner: &CodeUnit,
member: &CodeUnit,
) -> bool {
analyzer.direct_children(owner).into_iter().any(|child| {
child.kind() == member.kind()
&& child.identifier() == member.identifier()
&& child.signature() == member.signature()
})
}
fn classify_direct_owner_candidates(
analyzer: &CppGraphSource<'_>,
candidates: impl Iterator<Item = CodeUnit>,
) -> DirectOwnerResolution {
collapse_owner_candidates(candidates.map(|candidate| {
let strength = cpp_class_declaration_strength(analyzer, &candidate);
(candidate, strength)
}))
}
pub fn collapse_owner_candidates(
candidates: impl Iterator<Item = (CodeUnit, CppClassDeclarationStrength)>,
) -> DirectOwnerResolution {
let mut full_definition = None;
let mut forwards = Vec::new();
for (candidate, strength) in candidates {
match strength {
CppClassDeclarationStrength::Full if full_definition.is_some() => {
return DirectOwnerResolution::Ambiguous;
}
CppClassDeclarationStrength::Full => full_definition = Some(candidate),
CppClassDeclarationStrength::Forward => forwards.push(candidate),
CppClassDeclarationStrength::Unknown => return DirectOwnerResolution::Ambiguous,
}
}
if let Some(owner) = full_definition {
DirectOwnerResolution::UniqueFull(owner)
} else if !forwards.is_empty() {
DirectOwnerResolution::ForwardsOnly(forwards)
} else {
DirectOwnerResolution::None
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn unique_logical_forward_owner_for_test(forwards: Vec<CodeUnit>) -> Option<CodeUnit> {
unique_logical_forward_owner(forwards)
}
fn unique_logical_forward_owner(mut forwards: Vec<CodeUnit>) -> Option<CodeUnit> {
let first = forwards.pop()?;
forwards
.iter()
.all(|forward| same_logical_symbol(forward, &first))
.then_some(first)
}
pub fn cpp_class_declaration_strength(
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> CppClassDeclarationStrength {
let Some(cpp) = analyzer.cpp else {
return uncached_cpp_class_declaration_strength(analyzer, candidate);
};
if let Some(strength) = cpp.cached_class_declaration_strength(candidate) {
return strength;
}
let strength = uncached_cpp_class_declaration_strength(analyzer, candidate);
cpp.cache_class_declaration_strength(candidate, strength);
strength
}
fn uncached_cpp_class_declaration_strength(
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> CppClassDeclarationStrength {
if let Some(facts) = analyzer.declaration_source_properties(candidate) {
if facts
.iter()
.any(|fact| fact.class_strength == CppClassDeclarationStrength::Full)
{
return CppClassDeclarationStrength::Full;
}
if facts
.iter()
.any(|fact| fact.class_strength == CppClassDeclarationStrength::Forward)
{
return CppClassDeclarationStrength::Forward;
}
}
if let Some(cpp) = analyzer.cpp
&& let Some(prepared) = cpp.prepared_syntax(analyzer.token, candidate.source())
{
return cpp_class_declaration_strength_in_tree(
analyzer,
&cpp.recovered_export_class_index(analyzer.token, candidate.source()),
candidate,
prepared.source(),
prepared.tree().root_node(),
);
}
let Some(source) = analyzer.indexed_source(candidate.source()) else {
return CppClassDeclarationStrength::Unknown;
};
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.is_err()
{
return CppClassDeclarationStrength::Unknown;
}
let Some(tree) = parser.parse(&source, None) else {
return CppClassDeclarationStrength::Unknown;
};
let recovered_export_classes =
CppRecoveredExportClassIndex::build(tree.root_node(), source.as_str());
cpp_class_declaration_strength_in_tree(
analyzer,
&recovered_export_classes,
candidate,
&source,
tree.root_node(),
)
}
fn cpp_class_declaration_strength_in_tree(
analyzer: &CppGraphSource<'_>,
recovered_export_classes: &CppRecoveredExportClassIndex,
candidate: &CodeUnit,
source: &str,
root: Node<'_>,
) -> CppClassDeclarationStrength {
let ranges = analyzer.ranges(candidate);
let mut saw_forward = false;
for range in ranges {
match recovered_class_body_at(
recovered_export_classes,
root,
source,
candidate.identifier(),
&range,
) {
Some(true) => return CppClassDeclarationStrength::Full,
Some(false) => {
saw_forward = true;
continue;
}
None => {}
}
let covers_range_start = |node: &Node<'_>| {
node.start_byte() <= range.start_byte && node.end_byte() >= range.start_byte
};
let mut stack = Vec::new();
if covers_range_start(&root) {
stack.push(root);
}
while let Some(node) = stack.pop() {
if node.start_byte() == range.start_byte
&& node.end_byte() == range.end_byte
&& matches!(
node.kind(),
"class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
)
{
if cpp_class_node_has_body(node) {
return CppClassDeclarationStrength::Full;
}
saw_forward = true;
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor).filter(covers_range_start));
}
}
if saw_forward {
CppClassDeclarationStrength::Forward
} else {
CppClassDeclarationStrength::Unknown
}
}
fn cpp_class_node_has_body(node: Node<'_>) -> bool {
child_by_field_or_named_child_of_kinds(
node,
"body",
&[
"declaration_list",
"field_declaration_list",
"enumerator_list",
],
)
.is_some()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CppCTagKind {
Struct,
Union,
}
fn indexed_c_tag_kind(analyzer: &CppGraphSource<'_>, code_unit: &CodeUnit) -> Option<CppCTagKind> {
let declaration = analyzer.get_source(code_unit, false)?;
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.ok()?;
let tree = parser.parse(&declaration, None)?;
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
let kind = match node.kind() {
"struct_specifier" => CppCTagKind::Struct,
"union_specifier" => CppCTagKind::Union,
_ => {
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
continue;
}
};
if node
.child_by_field_name("name")
.is_some_and(|name| node_text(name, &declaration) == code_unit.identifier())
{
return Some(kind);
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
None
}
pub fn visible_owner_from_member_name(ctx: &ScanCtx<'_>, code_unit: &CodeUnit) -> Option<CodeUnit> {
if !code_unit.owner_is_type_scope() {
return None;
}
let owner_fq = code_unit.fq().parent()?;
ctx.analyzer
.workspace_definitions()
.exact(&owner_fq)
.into_iter()
.find(|candidate| candidate.is_class() && ctx.visibility.is_visible(ctx.file, candidate))
}
pub fn same_symbol(left: &CodeUnit, right: &CodeUnit) -> bool {
same_logical_symbol(left, right) && left.source() == right.source()
}
pub fn same_visible_symbol(left: &CodeUnit, right: &CodeUnit) -> bool {
same_symbol(left, right) || same_logical_symbol(left, right)
}
pub fn same_visible_global_field_symbol(
analyzer: &CppGraphSource<'_>,
internal_linkage_cache: &mut HashMap<CodeUnit, bool>,
left: &CodeUnit,
right: &CodeUnit,
) -> bool {
if same_symbol(left, right) {
return true;
}
if !same_logical_symbol(left, right) {
return false;
}
if cpp_global_field_has_internal_linkage_cached(analyzer, internal_linkage_cache, left)
|| cpp_global_field_has_internal_linkage_cached(analyzer, internal_linkage_cache, right)
{
left.source() == right.source()
} else {
true
}
}
fn cpp_global_field_has_internal_linkage_cached(
analyzer: &CppGraphSource<'_>,
cache: &mut HashMap<CodeUnit, bool>,
candidate: &CodeUnit,
) -> bool {
if let Some(internal) = cache.get(candidate) {
return *internal;
}
#[cfg(any(test, feature = "test-support"))]
note_cpp_global_field_internal_linkage_classification_for_test();
let internal = cpp_global_field_has_internal_linkage(analyzer, candidate);
cache.insert(candidate.clone(), internal);
internal
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static CPP_GLOBAL_FIELD_INTERNAL_LINKAGE_CLASSIFICATIONS_FOR_TEST: Cell<usize> = const { Cell::new(0) };
}
#[cfg(any(test, feature = "test-support"))]
fn note_cpp_global_field_internal_linkage_classification_for_test() {
CPP_GLOBAL_FIELD_INTERNAL_LINKAGE_CLASSIFICATIONS_FOR_TEST.with(|count| {
count.set(count.get() + 1);
});
}
#[cfg(any(test, feature = "test-support"))]
pub fn with_cpp_global_field_internal_linkage_classification_counter_for_test<T>(
body: impl FnOnce() -> T,
) -> (T, usize) {
with_thread_local_counter_for_test(
&CPP_GLOBAL_FIELD_INTERNAL_LINKAGE_CLASSIFICATIONS_FOR_TEST,
body,
)
}
#[cfg(any(test, feature = "test-support"))]
fn with_thread_local_counter_for_test<T>(
counter: &'static std::thread::LocalKey<Cell<usize>>,
body: impl FnOnce() -> T,
) -> (T, usize) {
counter.with(|count| {
count.set(0);
let result = body();
let observed = count.get();
count.set(0);
(result, observed)
})
}
pub fn same_logical_symbol(left: &CodeUnit, right: &CodeUnit) -> bool {
left.kind() == right.kind()
&& left.fq_name() == right.fq_name()
&& left.signature() == right.signature()
}
pub fn select_alias_spelling_with_agreed_targets<'a, T: Eq>(
candidates: &[&'a CodeUnit],
mut structured_target: impl FnMut(&CodeUnit) -> Option<T>,
) -> Option<&'a CodeUnit> {
let first = *candidates.first()?;
if candidates.len() == 1 {
return Some(first);
}
let first_target = structured_target(first)?;
candidates
.iter()
.skip(1)
.all(|candidate| structured_target(candidate).is_some_and(|target| target == first_target))
.then_some(first)
}
pub fn cpp_global_field_has_internal_linkage(
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> bool {
if !candidate.is_field() || candidate.short_name().contains('.') {
return false;
}
let Some(local_linkage) = cpp_global_field_declaration_linkage(analyzer, candidate) else {
return false;
};
match local_linkage {
CppFieldLinkage::Internal => true,
CppFieldLinkage::External => false,
CppFieldLinkage::InternalUnlessExternalPeer => {
!cpp_global_field_linkage_peers(analyzer, candidate)
.filter_map(|peer| cpp_global_field_declaration_linkage(analyzer, &peer))
.any(|linkage| matches!(linkage, CppFieldLinkage::External))
}
}
}
fn cpp_global_field_linkage_peers<'a>(
analyzer: &CppGraphSource<'a>,
candidate: &'a CodeUnit,
) -> impl Iterator<Item = CodeUnit> + 'a {
let name = candidate.fq().clone();
analyzer
.workspace_definitions()
.exact(&name)
.into_iter()
.filter(move |peer| {
if peer == candidate {
return false;
}
#[cfg(any(test, feature = "test-support"))]
note_cpp_global_field_linkage_peer_inspection_for_test();
same_logical_symbol(peer, candidate)
})
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static CPP_GLOBAL_FIELD_LINKAGE_PEER_INSPECTIONS_FOR_TEST: Cell<usize> = const { Cell::new(0) };
}
#[cfg(any(test, feature = "test-support"))]
fn note_cpp_global_field_linkage_peer_inspection_for_test() {
CPP_GLOBAL_FIELD_LINKAGE_PEER_INSPECTIONS_FOR_TEST.with(|count| {
count.set(count.get() + 1);
});
}
#[cfg(any(test, feature = "test-support"))]
pub fn with_cpp_global_field_linkage_peer_inspection_counter_for_test<T>(
body: impl FnOnce() -> T,
) -> (T, usize) {
with_thread_local_counter_for_test(&CPP_GLOBAL_FIELD_LINKAGE_PEER_INSPECTIONS_FOR_TEST, body)
}
fn cpp_global_field_declaration_linkage(
analyzer: &CppGraphSource<'_>,
candidate: &CodeUnit,
) -> Option<CppFieldLinkage> {
analyzer.cpp_field_linkage(candidate)
}
pub fn cpp_canonical_alias_target_text(
analyzer: &CppGraphSource<'_>,
unit: &CodeUnit,
) -> Option<String> {
let facts = analyzer.declaration_source_properties(unit)?;
let first_fact = facts.first()?;
let first = first_fact.alias_target_text.as_ref()?;
let normalized = normalize_cpp_whitespace(first);
facts
.iter()
.all(|fact| {
fact.alias_target_text.as_ref().is_some_and(|target| {
target == first
|| (first_fact.alias_target.is_some()
&& fact.alias_target == first_fact.alias_target
&& normalize_cpp_whitespace(target) == normalized)
})
})
.then(|| first.clone())
}
#[cfg(test)]
mod tests {
#[test]
fn resolver_displaced_conditional_grammar_boundary() {
let source = "#ifdef FLAG\nint values[\n#endif\n1];\nint outside;\n";
let tree = parse_cpp(source);
let conditional = tree.root_node().named_child(0).unwrap();
assert_eq!(conditional.kind(), "preproc_ifdef");
assert!(conditional.has_error());
let boundary = cpp_displaced_preprocessor_boundary(conditional).unwrap();
let directive = crate::grammar_test_support::nodes(conditional)
.into_iter()
.find(|node| node.kind() == "#endif" && !node.is_missing())
.unwrap();
assert_eq!(directive.parent().unwrap().kind(), "ERROR");
assert_eq!(directive.end_byte(), boundary.end_byte);
let inside = named_node_at(&tree, source, "values");
let outside = named_node_at(&tree, source, "outside");
assert!(has_ancestor_kind(inside, "preproc_ifdef"));
assert!(has_ancestor_kind(outside, "preproc_ifdef"));
assert!(inside.end_byte() < directive.start_byte());
assert!(outside.start_byte() > directive.end_byte());
assert!(preprocessor_conditional_contains_descendant(
conditional,
inside
));
assert!(!preprocessor_conditional_contains_descendant(
conditional,
outside
));
}
#[test]
fn resolver_recovered_namespace_boundaries() {
let source = STOLEN_BRACE_CASCADE;
let tree = parse_cpp(source);
let index = OrphanedNamespaceScopeIndex::build(tree.root_node(), source);
assert!(!index.regions.is_empty(), "fixture must enter recovery");
for (needle, expected) in [
("First", vec!["app", "matchers", "detail"]),
("Second", vec!["app", "matchers", "detail"]),
("tail_function", vec!["app", "matchers"]),
("TailClass", vec!["app", "matchers"]),
("AfterAll", vec![]),
] {
let node = named_node_at(&tree, source, needle);
let mut parsed: Vec<_> = std::iter::successors(node.parent(), |node| node.parent())
.filter(|node| node.kind() == "namespace_definition")
.filter_map(|namespace| {
let name = namespace.child_by_field_name("name")?;
let mut components = Vec::new();
append_cpp_name_components(name, source, &mut components)?;
Some((
namespace.start_byte(),
components,
namespace_definition_has_real_braces(namespace),
))
})
.collect();
parsed.reverse();
assert_eq!(
index.restore_enclosing_namespaces(parsed, node.start_byte()),
expected,
"{needle}"
);
}
for region in &index.regions {
assert!(region.start < region.end);
assert!(index.region_at(region.start).is_some());
assert!(
index
.region_at(region.end)
.is_none_or(|found| !std::ptr::eq(found, region)),
"region leaked beyond its half-open boundary: {region:?}"
);
}
let errors: Vec<_> = crate::grammar_test_support::nodes(tree.root_node())
.into_iter()
.filter(|node| node.kind() == "ERROR")
.filter(|node| {
let mut cursor = node.walk();
node.children(&mut cursor).any(|child| child.kind() == "}")
})
.collect();
assert!(
!errors.is_empty(),
"fixture must retain unmatched close tokens"
);
for error in errors {
assert!(direct_unmatched_closing_brace(error), "{error:?}");
}
}
#[test]
fn resolver_declarator_grammar_boundary_table() {
for (source, name, kind, binder) in [
("int first, values[3];", "values", "identifier", true),
(
"void f() { int first, values[3]; }",
"values",
"identifier",
true,
),
(
"typedef unsigned Underlying; enum Named : Underlying { Value };",
"Underlying {",
"type_identifier",
false,
),
] {
let tree = parse_cpp(source);
assert!(!tree.root_node().has_error(), "{source}");
let spelling = name.split_whitespace().next().unwrap();
let start = source.find(name).unwrap();
let node = tree
.root_node()
.named_descendant_for_byte_range(start, start + spelling.len())
.unwrap();
assert_eq!(node.kind(), kind, "{source}");
if spelling == "values" {
let array = node.parent().unwrap();
assert_eq!(array.kind(), "array_declarator");
assert_eq!(array.child_by_field_name("declarator"), Some(node));
assert!(is_declarator_node(array), "{source}");
} else {
let enumeration = node.parent().unwrap();
assert_eq!(enumeration.kind(), "enum_specifier");
assert_eq!(enumeration.child_by_field_name("base"), Some(node));
assert_eq!(
enumeration.child_by_field_name("name").is_some(),
source.contains("enum Named")
);
}
assert_eq!(is_declaration_name(node), binder, "{source}");
}
}
#[test]
fn resolver_alias_scope_and_argument_grammar_table() {
for (source, scope) in [
("template<class T> using Alias = T;", true),
("namespace n { template<class T> using Alias = T; }", true),
(
"struct Owner { template<class T> using Alias = T; };",
false,
),
("void f() { using Alias = int; }", false),
] {
let tree = parse_cpp(source);
assert!(!tree.root_node().has_error(), "{source}");
let alias = crate::grammar_test_support::nodes(tree.root_node())
.into_iter()
.find(|node| node.kind() == "alias_declaration")
.unwrap();
assert_eq!(
node_text(alias.child_by_field_name("name").unwrap(), source),
"Alias"
);
assert!(alias.child_by_field_name("type").is_some());
assert_eq!(alias_has_visible_file_scope(alias), scope, "{source}");
}
for (source, argument_kinds, expression) in [
(
"ns::Alias value(42, arg);",
vec!["number_literal", "identifier"],
true,
),
(
"ns::Alias value(42, 7);",
vec!["number_literal", "number_literal"],
false,
),
("ns::Alias value();", vec![], false),
] {
let tree = parse_cpp(source);
assert!(!tree.root_node().has_error(), "{source}");
let declaration = tree.root_node().named_child(0).unwrap();
let ty = declaration.child_by_field_name("type").unwrap();
assert_eq!(node_text(ty, source), "ns::Alias");
let argument_lists: Vec<_> = crate::grammar_test_support::nodes(declaration)
.into_iter()
.filter(|node| node.kind() == "argument_list")
.collect();
let kinds: Vec<_> = argument_lists
.iter()
.flat_map(|node| {
let mut cursor = node.walk();
node.named_children(&mut cursor)
.map(|child| child.kind())
.collect::<Vec<_>>()
})
.collect();
assert_eq!(kinds, argument_kinds, "{source}");
assert_eq!(
qualified_alias_constructor_has_expression_argument(ty),
expression,
"{source}"
);
}
}
#[test]
fn resolver_namespace_brace_grammar_table() {
for (source, complete, closing_missing) in [
("namespace n { int item; }", true, false),
("namespace n { int item;", false, true),
] {
let tree = parse_cpp(source);
let namespace = tree.root_node().named_child(0).unwrap();
assert_eq!(namespace.kind(), "namespace_definition");
let body = namespace.child_by_field_name("body").unwrap();
assert_eq!(body.kind(), "declaration_list");
let open = body.child(0).unwrap();
let close = body.child(body.child_count() - 1).unwrap();
assert_eq!(open.kind(), "{");
assert!(!open.is_missing());
assert_eq!(close.kind(), "}");
assert_eq!(close.is_missing(), closing_missing);
assert_eq!(
namespace_definition_has_real_braces(namespace),
complete,
"{source}"
);
}
let source = "namespace { namespace detail { struct Derived {}; void run() {} } }";
let tree = parse_cpp(source);
assert!(!tree.root_node().has_error());
let anonymous = tree.root_node().named_child(0).unwrap();
assert_eq!(anonymous.kind(), "namespace_definition");
assert!(anonymous.child_by_field_name("name").is_none());
let run = named_node_at(&tree, source, "run");
assert_eq!(
enclosing_namespace_components(run, source),
Some(vec!["detail".to_owned()])
);
}
#[test]
fn resolver_c_block_envelope_grammar_table() {
for (source, name, damaged, recovered) in [
("void outer(void) { nested() {} }", "nested", false, false),
(
"void outer(void) { Apply(first, ^(Bucket b) { return b.value; }); }",
"Apply",
true,
true,
),
] {
let tree = parse_cpp(source);
let node = named_node_at(&tree, source, name);
let declarator = node.parent().unwrap();
assert_eq!(
declarator.kind(),
"function_declarator",
"{source}: {:?}",
tree.root_node()
);
assert_eq!(declarator.child_by_field_name("declarator"), Some(node));
assert_eq!(declarator.has_error(), damaged, "{source}");
let definition = declarator.parent().unwrap();
assert_eq!(definition.kind(), "function_definition");
assert!(definition.child_by_field_name("type").is_none());
assert!(definition.child_by_field_name("body").is_some());
assert_eq!(
recovered_c_block_call_envelope(declarator).is_some(),
recovered,
"{source}"
);
}
}
#[test]
fn resolver_linkage_guard_grammar_table() {
for (source, recovered, visible) in [
(
"#ifdef __cplusplus\nextern \"C\" {\n#endif\nvoid target();\n#ifdef __cplusplus\n}\n#endif\n",
false,
true,
),
(
"#ifdef __cplusplus\nextern \"C\" {\nvoid target();\n#ifdef __cplusplus\n}\n#endif\n",
true,
false,
),
(
"#ifdef __cplusplus\nextern \"C\" {\nvoid target();\n}\n#endif\n",
true,
false,
),
] {
let tree = parse_cpp(source);
let name = named_node_at(&tree, source, "target");
let mut declaration = name;
while declaration.kind() != "declaration" {
declaration = declaration.parent().unwrap();
}
assert_eq!(
has_ancestor_kind(declaration, "linkage_specification"),
recovered,
"{source}"
);
assert_eq!(
has_ancestor_kind(declaration, "preproc_ifdef"),
recovered,
"{source}"
);
if !recovered {
assert!(!tree.root_node().has_error());
let open = tree.root_node().named_child(0).unwrap();
assert_eq!(open.kind(), "preproc_linkage_open");
assert!(open.child_by_field_name("value").is_some());
}
assert_eq!(
callable_preprocessor_context_is_visible(declaration, source),
visible,
"{source}"
);
}
}
#[test]
#[hardwon("#6902: grammar 0.24.4 parsed an unnamed enum's base as its name")]
fn resolver_anonymous_enum_base_grammar_boundary() {
let source = "typedef unsigned Underlying; enum : Underlying { Value };";
let tree = parse_cpp(source);
assert!(
!tree.root_node().has_error(),
"{}",
tree.root_node().to_sexp()
);
let start = source.find("Underlying {").unwrap();
let base = tree
.root_node()
.named_descendant_for_byte_range(start, start + "Underlying".len())
.unwrap();
let enumeration = base.parent().unwrap();
assert_eq!(enumeration.kind(), "enum_specifier");
assert!(enumeration.child_by_field_name("name").is_none());
assert_eq!(enumeration.child_by_field_name("base"), Some(base));
assert!(!is_declaration_name(base));
}
#[test]
#[hardwon(
"e3c20990a105: quicksilver round 4 on brokk-bifrost-cpp: spliced macro tokens need physical source coordinates"
)]
fn macro_splices_preserve_statement_origin_ranges() {
for newline in ["\n", "\r\n"] {
for (replacement, name, written) in [
(
format!("jo\\{newline}ined\\{newline}();\\{newline}after();"),
"joined",
format!("jo\\{newline}ined"),
),
(
format!("do {{ if (ready) bl\\{newline}ock }} while (0)"),
"block",
format!("bl\\{newline}ock"),
),
] {
let parameters = vec!["block".to_owned()];
let (source, tree, original_offsets) =
VisibilityIndex::parse_macro_replacement_body(&replacement, ¶meters)
.expect("joined replacement parses");
let body = ParsedReplacementBody {
source,
tree,
body_offset: MACRO_BODY_SENTINEL_PREFIX.len(),
parameters,
original_offsets,
};
let nodes = crate::grammar_test_support::nodes(body.tree.root_node());
let matching: Vec<_> = nodes
.iter()
.filter(|node| {
node.kind() == "identifier" && node_text(**node, &body.source) == name
})
.collect();
assert_eq!(matching.len(), 1, "{replacement}");
let range = body.file_range(*matching[0], 0);
assert_eq!(&replacement[range], written);
for node in nodes.iter().filter(|node| {
node.kind() == "identifier" && node_text(**node, &body.source) == "after"
}) {
assert_eq!(&replacement[body.file_range(*node, 0)], "after");
}
}
}
}
#[test]
#[hardwon("05fbd05c6bd7: C++ declaration recovery regressed for this structured syntax edge")]
fn issue_3089_statement_formal_at_end_of_replacement() {
let parameters = vec!["handle".to_owned(), "block".to_owned()];
for replacement in [
"do { header_event_t* event; if ((handle)->active) block } while (0)",
"do { header_event_t* event; block } while (0)",
] {
assert!(
super::VisibilityIndex::parse_macro_replacement_body(replacement, ¶meters)
.is_some(),
"{replacement}"
);
}
}
use super::*;
use brokk_bifrost_test_macros::hardwon;
#[test]
#[hardwon("2e5169325a14: C++ declaration recovery regressed for this structured syntax edge")]
fn c_sizeof_expression_type_candidate_is_structural_and_c_only() {
let source = "int size(void) { return sizeof(((Payload))); }\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let start = source.find("Payload").expect("sizeof operand");
let node = tree
.root_node()
.named_descendant_for_byte_range(start, start + "Payload".len())
.expect("focused operand");
let c_file = ProjectFile::new(std::env::temp_dir(), "issue.c");
let cpp_file = ProjectFile::new(std::env::temp_dir(), "issue.cpp");
assert_eq!(node.kind(), "identifier");
assert!(is_c_sizeof_expression_type_candidate(&c_file, node));
assert!(!is_c_sizeof_expression_type_candidate(&cpp_file, node));
}
#[test]
fn recovered_macro_actuals_keep_their_cst_boundaries() {
for (source, recovered) in [
("int f() { return M(Type, value); }", false),
("int f() { return M(Type, ^(int n){ return n; }); }", true),
] {
let tree = parse_cpp(source);
let first = named_node_at(&tree, source, "Type");
let (call, index, actual) = c_function_macro_argument(first)
.unwrap_or_else(|| panic!("first actual {source}: {}", tree.root_node().to_sexp()));
assert_eq!(index, 0);
assert_eq!(node_text(actual, source), "Type");
let arguments = call.child_by_field_name("arguments").unwrap();
let raw = arguments.named_child(0).unwrap();
if recovered {
assert_eq!(raw.kind(), "binary_expression");
assert_eq!(raw.child_by_field_name("left"), Some(actual));
let inside = named_node_at(&tree, source, "return n;");
let (_, index, block) = c_function_macro_argument(inside).expect("block actual");
assert_eq!(index, 1);
assert_eq!(block.kind(), "compound_literal_expression");
assert_eq!(node_text(block, source), "(int n){ return n; }");
assert_eq!(raw.child_by_field_name("right"), Some(block));
} else {
assert_eq!(raw, actual);
let second = named_node_at(&tree, source, "value");
let (_, index, actual) = c_function_macro_argument(second).expect("second actual");
assert_eq!(index, 1);
assert_eq!(node_text(actual, source), "value");
}
}
}
fn parse_cpp(source: &str) -> Tree {
crate::diagnostics::parse_cpp_tree(source)
}
#[test]
#[hardwon("#4271: source splitting included a new-array declarator in the allocation type")]
fn cpp_new_expression_type_comes_from_the_structured_type_field() {
let source = "auto value = new ns::Box<int> [ 3 ];";
let tree = parse_cpp(source);
let mut pending = vec![tree.root_node()];
let allocation = loop {
let node = pending.pop().expect("new expression in fixture");
if node.kind() == "new_expression" {
break node;
}
let mut cursor = node.walk();
pending.extend(
node.named_children(&mut cursor)
.collect::<Vec<_>>()
.into_iter()
.rev(),
);
};
let type_node = cpp_new_expression_type_node(allocation).expect("structured type field");
assert_eq!(allocation.child_by_field_name("type"), Some(type_node));
assert_eq!(node_text(type_node, source), "ns::Box<int>");
assert_eq!(
allocation
.child_by_field_name("declarator")
.expect("array declarator")
.kind(),
"new_declarator"
);
let old_split = normalize_cpp_whitespace(node_text(allocation, source));
let old_split = old_split
.strip_prefix("new ")
.unwrap_or(old_split.as_str())
.split(['(', '{'])
.next()
.expect("old split result");
assert_ne!(old_split.trim(), node_text(type_node, source));
}
fn named_node_at<'tree>(tree: &'tree Tree, source: &str, needle: &str) -> Node<'tree> {
let start = source.find(needle).expect("fixture needle");
tree.root_node()
.named_descendant_for_byte_range(start, start + needle.len())
.expect("node at needle")
}
fn prepared_cpp(source: &str) -> PreparedSyntaxTree {
let tree = crate::diagnostics::parse_cpp_tree(source);
PreparedSyntaxTree::new(
PreparedSyntaxSource::Exact(Arc::from(source)),
tree,
compute_line_starts(source),
LanguageDialect::Standard(Language::Cpp),
PreparedSourceOrigin::Disk,
None,
)
}
fn unresolved_include_before(source: &str, reference: &str) -> bool {
let file = ProjectFile::new(std::env::temp_dir(), "issue-3078.cpp");
let prepared = prepared_cpp(source);
let facts = collect_structured_include_facts(&prepared);
let include_targets = IncludeTargetIndex::build([&file]);
has_unresolved_include_visible_before_in_prepared(
&file,
&prepared,
&include_targets,
&facts,
source.find(reference).expect("reference fixture"),
&|guards| guards.cloned(),
)
}
#[test]
#[hardwon("5f8417432427: C++ declaration recovery regressed for this structured syntax edge")]
fn unresolved_include_before_reference_is_visible() {
let source = "#include \"missing.h\"\nint use = Missing;\n";
assert!(unresolved_include_before(source, "Missing"));
}
#[test]
#[hardwon("5f8417432427: C++ declaration recovery regressed for this structured syntax edge")]
fn unresolved_include_after_reference_is_not_visible() {
let source = "int use = Missing;\n#include \"missing.h\"\n";
assert!(!unresolved_include_before(source, "Missing"));
}
#[test]
#[hardwon("5f8417432427: C++ declaration recovery regressed for this structured syntax edge")]
fn unresolved_include_in_incompatible_sibling_branch_is_not_visible() {
let source = "#if FEATURE\n#include \"missing.h\"\n#else\nint use = Missing;\n#endif\n";
assert!(!unresolved_include_before(source, "Missing"));
}
#[test]
#[hardwon("5f8417432427: C++ declaration recovery regressed for this structured syntax edge")]
fn unresolved_include_in_current_branch_is_visible() {
let source =
"#if FEATURE\n#include \"missing.h\"\nint use = Missing;\n#else\nint other;\n#endif\n";
assert!(unresolved_include_before(source, "Missing"));
}
const STOLEN_BRACE_CASCADE: &str = r#"namespace app {
namespace matchers {
namespace detail {
class API [[nodiscard]] First {
public:
int value() const { return count_ + 1; }
private:
int count_;
};
class API [[nodiscard]] Second {
public:
int value() const { return count_ + 2; }
private:
int count_;
};
} // namespace detail
template <typename T>
void tail_function(MatcherBase<T> const& value);
class TailClass {};
} // namespace matchers
} // namespace app
struct AfterAll {};
"#;
#[test]
#[hardwon("c820d8c6a8e3: C++ declaration recovery regressed for this structured syntax edge")]
fn orphaned_namespace_scope_index_restores_a_stolen_brace_cascade() {
let source = STOLEN_BRACE_CASCADE;
let tree = parse_cpp(source);
let index = OrphanedNamespaceScopeIndex::build(tree.root_node(), source);
let tail_class = named_node_at(&tree, source, "TailClass");
assert!(
!has_ancestor_kind(tail_class, "namespace_definition"),
"the fixture must reproduce the recovery: the tail has no namespace ancestor"
);
let displaced = named_node_at(&tree, source, "Second");
assert_eq!(
enclosing_namespace_components(displaced, source),
Some(vec!["app".to_string(), "matchers".to_string()]),
"the fixture must displace the second class out of detail"
);
let components = |needle: &str| {
index.enclosing_namespace_components(named_node_at(&tree, source, needle), source)
};
assert_eq!(components("First"), ["app", "matchers", "detail"]);
assert_eq!(components("Second"), ["app", "matchers", "detail"]);
assert_eq!(components("MatcherBase<T>"), ["app", "matchers"]);
assert_eq!(components("tail_function"), ["app", "matchers"]);
assert_eq!(components("TailClass"), ["app", "matchers"]);
assert!(components("AfterAll").is_empty());
}
#[test]
#[hardwon("c820d8c6a8e3: C++ declaration recovery regressed for this structured syntax edge")]
fn orphaned_namespace_scope_index_is_empty_without_lost_scopes() {
let clean = "namespace a { namespace b { class C {}; } class D {}; }\n";
let tree = parse_cpp(clean);
assert!(!tree.root_node().has_error());
assert!(OrphanedNamespaceScopeIndex::build(tree.root_node(), clean).is_empty());
let damaged = "namespace a { namespace b { UNKNOWN_MACRO(x) } class C {}; }\n";
let tree = parse_cpp(damaged);
assert!(tree.root_node().has_error());
let index = OrphanedNamespaceScopeIndex::build(tree.root_node(), damaged);
assert_eq!(
index.enclosing_namespace_components(named_node_at(&tree, damaged, "class C"), damaged),
["a"]
);
}
const COLLAPSED_NAMESPACE_HEAD: &str = r#"namespace app {
class Target {
int value_;
};
template <typename T>
class Holder {
public:
explicit constexpr Holder( T lhs ): m_lhs( lhs ) {}
#define HOLDER_DEFINE_OP( id, op ) \
template <typename U> \
constexpr friend auto operator op( Holder&& lhs, U&& rhs ) \
-> std::enable_if_t<is_##id##_comparable<T, U>::value, Target> { \
return Target{}; \
}
HOLDER_DEFINE_OP( equal, == )
#undef HOLDER_DEFINE_OP
T m_lhs;
};
class Tail {};
}
"#;
#[test]
#[hardwon("a438f125e172: C++ declaration recovery regressed for this structured syntax edge")]
fn orphaned_namespace_scope_index_names_a_collapsed_namespace_head() {
let source = COLLAPSED_NAMESPACE_HEAD;
let tree = parse_cpp(source);
let target = named_node_at(&tree, source, "class Target");
assert!(
tree.root_node().has_error(),
"the fixture must retain the macro invocation's parse damage"
);
assert!(
has_ancestor_kind(target, "namespace_definition"),
"the namespace head survives: the class keeps its parsed namespace ancestor"
);
let index = OrphanedNamespaceScopeIndex::build(tree.root_node(), source);
assert_eq!(
index.enclosing_namespace_components(target, source),
["app"]
);
assert_eq!(
index.enclosing_namespace_levels(target, source),
vec![(
"app".to_string(),
Range {
start_byte: 0,
end_byte: source.len() - 1,
start_line: 1,
end_line: 25,
},
Range {
start_byte: 10,
end_byte: 13,
start_line: 1,
end_line: 1,
},
)],
"the recovered namespace owns its full construct, not its first member or just its header"
);
}
#[test]
#[hardwon("#5868: a literal namespace head inside a damaged function wrapper lost its owner")]
fn namespace_body_name_components_recovers_literal_function_head() {
let source = "OPAQUE_PREFIX\nnamespace recovered\n{\nint member;\n}\nint member;\n";
let tree = parse_cpp(source);
let keyword_start = source.find("namespace recovered").unwrap();
let keyword = tree
.root_node()
.named_descendant_for_byte_range(keyword_start, keyword_start + "namespace".len())
.expect("literal keyword node");
assert_eq!(keyword.kind(), "type_identifier");
assert_eq!(keyword.child_count(), 0);
assert!(!keyword.is_missing() && !keyword.has_error());
assert_eq!(node_text(keyword, source), "namespace");
let wrapper = keyword.parent().expect("actual wrapper");
assert_eq!(wrapper.kind(), "function_definition");
let mut cursor = wrapper.walk();
let children = wrapper
.children(&mut cursor)
.filter(|child| child.kind() != "comment")
.collect::<Vec<_>>();
assert_eq!(
children.len(),
4,
"all named and unnamed children accounted: {children:?}"
);
let prefix = children[0];
assert_eq!(prefix.kind(), "macro_modifier");
assert!(!prefix.is_missing() && !prefix.has_error());
assert_eq!(node_text(prefix, source), "OPAQUE_PREFIX");
assert_eq!(children[1], keyword);
assert_eq!(wrapper.child_by_field_name("type"), Some(keyword));
let name = children[2];
assert!(matches!(name.kind(), "identifier" | "namespace_identifier"));
assert_eq!(name.child_count(), 0);
assert!(!name.is_missing() && !name.has_error());
assert_eq!(node_text(name, source), "recovered");
assert_eq!(wrapper.child_by_field_name("declarator"), Some(name));
let body = wrapper.child_by_field_name("body").expect("actual body");
assert_eq!(children[3], body);
assert_eq!(body.kind(), "compound_statement");
assert!(!body.is_missing());
let open = body.child(0).expect("actual first child");
assert_eq!(open.kind(), "{");
assert!(!open.is_missing() && !open.has_error());
assert_eq!(open.start_byte(), body.start_byte());
let (head, names) = namespace_body_name_components(wrapper, body, source)
.expect("strict synthetic literal head recognized");
assert_eq!(head, keyword);
assert_eq!(
names
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>(),
["recovered"]
);
assert_eq!(
(names[0].1.start_byte, names[0].1.end_byte),
(name.start_byte(), name.end_byte())
);
let inside = named_node_at(&tree, source, "int member;");
assert!(body.start_byte() < inside.start_byte() && inside.end_byte() < body.end_byte());
}
#[test]
#[hardwon("#5868: refusal controls must actually exercise the namespace body helper")]
fn orphaned_namespace_scope_index_refuses_unproven_function_heads() {
for refusal in [
"int ordinary(int value) { return value; }",
"OPAQUE_PREFIX namespace_ recovered { int member; }",
"OPAQUE_PREFIX namespace recovered other { int member; }",
"OPAQUE_PREFIX namespace recovered(int value) { return value; }",
] {
let tree = parse_cpp(refusal);
let mut stack = vec![tree.root_node()];
let mut helper_calls = 0;
while let Some(node) = stack.pop() {
if node.kind() == "function_definition"
&& let Some(body) = node.child_by_field_name("body")
{
helper_calls += 1;
assert_eq!(
(node.start_byte(), node.end_byte()),
(0, refusal.len()),
"guard control must exercise its complete envelope: source={refusal}, node={node:?}"
);
assert!(
namespace_body_name_components(node, body, refusal).is_none(),
"refused shape acquired namespace: source={refusal}, node={node:?}, body={body:?}"
);
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
assert_eq!(
helper_calls,
1,
"guard control must reach exactly its actual function/body helper call: source={refusal}, tree={:?}",
tree.root_node()
);
}
for absent in [
"OPAQUE_PREFIX namespace recovered = other { int member; }",
"OPAQUE_PREFIX namespace recovered int member;",
] {
let tree = parse_cpp(absent);
let mut stack = vec![tree.root_node()];
let mut helper_candidates = 0;
while let Some(node) = stack.pop() {
if node.kind() == "function_definition"
&& node.child_by_field_name("body").is_some()
{
helper_candidates += 1;
}
let mut cursor = node.walk();
stack.extend(node.named_children(&mut cursor));
}
assert_eq!(
helper_candidates,
0,
"parser-absence control unexpectedly acquired a helper envelope: source={absent}, tree={:?}",
tree.root_node()
);
}
}
const MACRO_OPENED_SCOPE: &str = r#"#define APP_IF_ACTIVE if (active) {
namespace app {
namespace detail {
bool check(bool active) {
APP_IF_ACTIVE
return true;
}
return false;
}
class Tail {};
} // namespace detail
} // namespace app
"#;
const REFLECTION_CASE_LABEL_RECOVERY: &str = r#"namespace simdjson {
namespace compile_time {
namespace number_parsing {
consteval int leading_zeroes(uint64_t input_num, int last_bit = 0) {
auto hex_to_u32 = [&] [[nodiscard]] () -> uint32_t {
auto digit = [](uint8_t c) -> uint32_t {
}
}
if (a) {
while (a) {
}
}
switch (c) {
case '{': {
}
}
}
while (a) {
cursor += field.second;
if (a) {
case '{': {
if (a) {
}
}
case 'n': {
if (a) {
}
}
}
}
if (a) {
simdjson_consteval_error("Expected '}'");
}
}
}
}
"#;
fn real_brace_token_counts(root: Node<'_>) -> (usize, usize) {
let mut stack = vec![root];
let mut counts = (0, 0);
while let Some(node) = stack.pop() {
match (node.kind(), node.is_missing()) {
("{", false) => counts.0 += 1,
("}", false) => counts.1 += 1,
_ => {}
}
let mut cursor = node.walk();
stack.extend(node.children(&mut cursor));
}
counts
}
fn namespace_open_byte(source: &str, head: &str) -> usize {
let at = source.find(head).expect("fixture namespace head");
at + head.len() - 1
}
fn assert_namespace_heads_have_no_matching_close(root: Node<'_>, source: &str, heads: &[&str]) {
let index = OrphanedNamespaceScopeIndex::build(root, source);
for head in heads {
assert_eq!(
index.matching_close_brace(namespace_open_byte(source, head)),
None,
"{head} must not claim a close belonging to an inner scope"
);
}
}
#[test]
#[hardwon("1352f739be3d: C++ declaration recovery regressed for this structured syntax edge")]
fn orphaned_namespace_scope_index_pairs_nothing_when_a_scope_open_is_lost() {
let source = MACRO_OPENED_SCOPE;
let tree = parse_cpp(source);
let root = tree.root_node();
assert_eq!(
real_brace_token_counts(root),
(4, 5),
"the fixture must reproduce the loss: one scope the source opens \
has no `{{` token, so the stream holds an extra real close"
);
assert_namespace_heads_have_no_matching_close(
root,
source,
&["namespace app {", "namespace detail {"],
);
}
#[test]
#[hardwon("5312adbcf9bf: C++ declaration recovery regressed for this structured syntax edge")]
fn orphaned_namespace_scope_index_leaves_a_declaration_after_every_close_at_file_scope() {
let source = format!("{MACRO_OPENED_SCOPE}struct AfterAll {{}};\n");
let tree = parse_cpp(&source);
let index = OrphanedNamespaceScopeIndex::build(tree.root_node(), &source);
let after_all = named_node_at(&tree, &source, "struct AfterAll");
assert!(
index
.enclosing_namespace_components(after_all, &source)
.is_empty()
);
}
#[test]
#[hardwon("1352f739be3d: C++ declaration recovery regressed for this structured syntax edge")]
fn orphaned_namespace_scope_index_preserves_structured_reflection_closes() {
let source = REFLECTION_CASE_LABEL_RECOVERY;
let tree = parse_cpp(source);
let root = tree.root_node();
let mut stack = vec![root];
let mut mis_lexed_opens = Vec::new();
while let Some(node) = stack.pop() {
if node.kind() == "character"
&& &source[node.start_byte()..node.end_byte()] == "{"
&& node
.parent()
.is_some_and(|parent| parent.kind() != "char_literal")
{
mis_lexed_opens.push(node.start_position().row + 1);
}
let mut cursor = node.walk();
stack.extend(node.children(&mut cursor));
}
assert!(
mis_lexed_opens.is_empty(),
"ABI15 should keep each case-label `{{` inside its `char_literal`; \
the old ABI14 mis-lex is no longer a valid recovery witness"
);
assert_eq!(
real_brace_token_counts(root),
(17, 17),
"the improved parse retains balanced real brace tokens"
);
assert_eq!(root.kind(), "translation_unit");
assert!(root.has_error(), "the malformed body remains incomplete");
let index = OrphanedNamespaceScopeIndex::build(root, source);
assert_eq!(
index.matching_close_brace(namespace_open_byte(source, "namespace simdjson {")),
None,
"a missing outer close must not borrow an inner namespace's real close"
);
for (head, close) in [
("namespace compile_time {", source.len() - 2),
("namespace number_parsing {", source.len() - 4),
] {
assert_eq!(
index
.matching_close_brace(namespace_open_byte(source, head))
.map(|range| range.start_byte),
Some(close),
"{head} retains its own real close"
);
}
}
#[test]
#[hardwon("#2243: an empty namespace could hide a nested indexed owner suffix")]
fn empty_parser_namespace_requires_a_nested_indexed_owner_suffix() {
let indexed = ["cache", "Outer", "Inner"].map(str::to_string);
assert!(indexed_namespace_path_is_recoverable(&[], &indexed, 2));
assert!(!indexed_namespace_path_is_recoverable(&[], &indexed, 1));
assert!(indexed_namespace_path_is_recoverable(
&["cache".to_string()],
&indexed,
1,
));
}
#[test]
#[hardwon("#1876: lookup ordering was partial and changed with unrelated workspace content")]
fn sort_lookup_units_totally_orders_every_identity_field() {
let file = ProjectFile::new(std::env::temp_dir(), "issue_1876.cpp");
let base = CodeUnit::with_signature(
file.clone(),
CodeUnitType::Function,
"scope",
"value",
Some("()".to_string()),
false,
);
let different_kind = CodeUnit::with_signature(
file.clone(),
CodeUnitType::Field,
"scope",
"value",
Some("()".to_string()),
false,
);
let synthetic = base.with_synthetic(true);
let interner = segment_interner();
let mut member_fq = FqName::new();
member_fq.push(interner.intern("scope", SegmentKind::Package));
member_fq.push(interner.intern("value", SegmentKind::Member));
let different_package_boundary = CodeUnit::from_fq(
file.clone(),
CodeUnitType::Function,
member_fq,
0,
Some("()".to_string()),
false,
);
let mut unknown_fq = FqName::new();
unknown_fq.push(interner.intern("scope", SegmentKind::Package));
unknown_fq.push(interner.intern("value", SegmentKind::Unknown));
let different_segment_kind = CodeUnit::from_fq(
file,
CodeUnitType::Function,
unknown_fq,
1,
Some("()".to_string()),
false,
);
let input = vec![
base,
different_kind,
synthetic,
different_package_boundary,
different_segment_kind,
];
let mut expected = input.clone();
sort_lookup_units(&mut expected);
assert!(expected.windows(2).all(|pair| {
let mut ordered = pair.to_vec();
sort_lookup_units(&mut ordered);
ordered == pair && pair[0] != pair[1]
}));
let mut reversed = input.clone();
reversed.reverse();
sort_lookup_units(&mut reversed);
assert_eq!(reversed, expected);
let mut rotated = input;
rotated.rotate_left(2);
sort_lookup_units(&mut rotated);
assert_eq!(rotated, expected);
}
#[test]
#[hardwon("6ffe85773652: C++ declaration recovery regressed for this structured syntax edge")]
fn structured_conditional_array_bound_preserves_the_real_guard() {
let damaged = "#ifndef API_H\n#define API_H\nextern char option_buffer[\n#ifdef FEATURE_X\n 16 +\n#endif\n 1];\n\nvoid target(void);\n#endif\n";
let guarded = "#ifdef FEATURE_X\nvoid target(void);\n#endif\n";
let parse = |source: &str| {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
parser.parse(source, None).expect("fixture tree")
};
let tree = parse(damaged);
let root = tree.root_node();
assert!(
!root.has_error(),
"the native conditional-array grammar now reads the original recovery witness cleanly: {}",
root.to_sexp()
);
let target = damaged.find("target").expect("target byte");
let declaration = root
.descendant_for_byte_range(target, target + "target".len())
.and_then(|mut node| {
loop {
if node.kind() == "declaration" {
break Some(node);
}
node = node.parent()?;
}
})
.expect("declaration after the displaced terminator");
let outer = declaration
.parent()
.filter(|node| node.kind() == "preproc_ifdef")
.expect("ordinary outer include guard");
let bound = outer
.named_children(&mut outer.walk())
.filter(|node| node.kind() == "declaration")
.filter_map(|node| node.child_by_field_name("declarator"))
.find(|node| node.kind() == "array_declarator")
.and_then(|node| node.child_by_field_name("size"))
.expect("structured conditional array bound");
assert_eq!(bound.kind(), "preproc_array_size");
let conditional = bound
.named_children(&mut bound.walk())
.find(|node| node.kind() == "preproc_ifdef")
.expect("array-size conditional");
assert!(conditional.end_byte() <= declaration.start_byte());
assert!(cpp_displaced_preprocessor_terminator(conditional).is_none());
assert!(
!std::iter::successors(Some(declaration), |node| node.parent())
.any(|ancestor| same_node(ancestor, conditional)),
"the declaration must not inherit the array-size feature guard"
);
assert!(cpp_displaced_preprocessor_terminator(outer).is_none());
assert!(preprocessor_conditional_contains_descendant(
outer,
declaration
));
let tree = parse(guarded);
let conditional = tree
.root_node()
.named_child(0)
.filter(|node| node.kind() == "preproc_ifdef")
.expect("ordinary conditional");
let declaration = conditional
.named_children(&mut conditional.walk())
.find(|node| node.kind() == "declaration")
.expect("guarded declaration");
assert!(cpp_displaced_preprocessor_terminator(conditional).is_none());
assert!(preprocessor_conditional_contains_descendant(
conditional,
declaration
));
let damaged_alternative = format!(
"#ifndef NO_FEATURE\nvoid enabled(void) {{}}\n#else\nvoid disabled(void) {{\n{}\n}}\n#endif\n",
"UNUSED(value)\n".repeat(64)
);
let tree = parse(&damaged_alternative);
let conditional = tree
.root_node()
.named_child(0)
.filter(|node| node.kind() == "preproc_ifdef")
.expect("outer conditional with an alternative");
assert!(conditional.has_error());
assert!(conditional.child_by_field_name("alternative").is_some());
assert!(
conditional
.child(conditional.child_count() - 1)
.is_some_and(|child| child.kind() == "#endif" && !child.is_missing())
);
assert!(cpp_displaced_preprocessor_terminator(conditional).is_none());
let split_declaration = "struct Node;\n\ntypedef\n #ifdef FEATURE_X\n struct Node *\n #else\n UInt32\n #endif\n NodeRef;\n\nstatic int target(void) { return 1; }\n#ifdef LATER\nint later;\n#endif\n";
let tree = parse(split_declaration);
let root = tree.root_node();
let split = root
.named_children(&mut root.walk())
.find(|node| node.kind() == "type_definition")
.expect("the split declaration folds into one type definition");
assert_eq!(
split.end_position().row + 1,
9,
"the split declaration ends at its own declarator line"
);
let target = split_declaration
.find("static int target")
.expect("target byte");
assert!(split.end_byte() <= target);
let target_node = root
.descendant_for_byte_range(target, target + "static".len())
.expect("target node");
assert!(
!std::iter::successors(Some(target_node), |node| node.parent())
.any(|ancestor| ancestor.kind() == "preproc_ifdef" && ancestor.has_error()),
"the declaration after the shredded split declaration must not \
inherit a damaged conditional ancestor"
);
}
#[test]
#[hardwon("6ba7704de3ed: C++ declaration recovery regressed for this structured syntax edge")]
fn fragmented_reference_guard_is_recovered() {
let source = "#if HAVE_ONE && HAVE_TWO\nstatic int helper(int value) { return value; }\n#endif\n\nint fragmented(int value) {\n if (value == 0) {\n return 0;\n#if HAVE_ONE && HAVE_TWO\n } else if (value == 1) {\n return helper(value);\n#endif\n }\n return 0;\n}\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let start = source.rfind("helper").expect("reference byte");
let node = tree
.root_node()
.descendant_for_byte_range(start, start + "helper".len())
.expect("reference node");
let mut expected = HashSet::default();
expected.insert(PreprocessorGuard::Boolean(BooleanGuardExpression::All(
vec![
BooleanGuardExpression::Truthy("HAVE_ONE".to_string()),
BooleanGuardExpression::Truthy("HAVE_TWO".to_string()),
],
)));
assert_eq!(preprocessor_guard_environment(node, source), Some(expected));
}
#[test]
#[hardwon(
"#6325: grammar 0.24.4 displaced #else/#elif into a declaration ERROR and the branch boundary vanished"
)]
fn displaced_branch_directives_decide_guards_without_proving_unreadable_branches() {
let parse = |source: &str| {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
parser.parse(source, None).expect("fixture tree")
};
fn at<'tree>(root: Node<'tree>, source: &str, needle: &str, token: &str) -> Node<'tree> {
let start = source.find(needle).expect("needle") + needle.find(token).expect("token");
root.descendant_for_byte_range(start, start + token.len())
.expect("token node")
}
let guards = |node: Node<'_>, source: &str| preprocessor_guard_environment(node, source);
let set = |items: &[PreprocessorGuard]| items.iter().cloned().collect::<HashSet<_>>();
let source = "namespace n {\n#ifdef A\nBEGIN_NS\nint x;\nEND_NS\n#else\nBEGIN_NS\nclass T {};\nEND_NS\n#define MODE 2\n#endif\n}\n";
let tree = parse(source);
let root = tree.root_node();
let x = at(root, source, "int x", "x");
let conditional = std::iter::successors(Some(x), |node| node.parent())
.find(|node| node.kind() == "preproc_ifdef")
.expect("ifdef");
assert!(conditional.child_by_field_name("alternative").is_none());
let directive = cpp_displaced_preprocessor_branch(conditional).expect("displaced #else");
assert_eq!(directive.kind(), "#else");
assert_eq!(directive.parent().map(|node| node.kind()), Some("ERROR"));
let defined = PreprocessorGuard::Defined("A".to_string());
assert_eq!(guards(x, source), Some(set(std::slice::from_ref(&defined))));
let class_name = at(root, source, "class T", "T");
assert_eq!(guards(class_name, source), Some(set(&[defined.negated()])));
let define = at(root, source, "#define MODE", "MODE")
.parent()
.filter(|node| node.kind() == "preproc_def")
.expect("macro event");
let mut cache = PreprocessorContextCache::default();
assert!(matches!(
preprocessor_branch_of_descendant(conditional, define, &mut cache),
PreprocessorBranch::DisplacedElse
));
assert_eq!(
preprocessor_conditional_family_for_declaration_with_cache(class_name, &mut cache),
Some(conditional),
"a displaced #else completes the family"
);
assert_eq!(
callable_guard_completion_byte(class_name, source),
Some(conditional.end_byte())
);
let source = "namespace n {\n#if A\nBEGIN_NS\nint x;\nEND_NS\n#elif B\nBEGIN_NS\nclass T {};\nEND_NS\n#else\nBEGIN_NS\nstruct T {};\nEND_NS\n#endif\n}\n";
let tree = parse(source);
let root = tree.root_node();
let x = at(root, source, "int x", "x");
let conditional = std::iter::successors(Some(x), |node| node.parent())
.find(|node| node.kind() == "preproc_if")
.expect("if");
assert!(conditional.child_by_field_name("alternative").is_none());
assert_eq!(
cpp_displaced_preprocessor_branch(conditional).map(|node| node.kind()),
Some("#elif")
);
let truthy = PreprocessorGuard::Boolean(BooleanGuardExpression::Truthy("A".to_string()));
assert_eq!(guards(x, source), Some(set(&[truthy])));
let mut cache = PreprocessorContextCache::default();
for needle in ["class T", "struct T"] {
let name = at(root, source, needle, "T");
assert_eq!(
guards(name, source),
None,
"{needle} after a displaced #elif"
);
assert!(matches!(
preprocessor_branch_of_descendant(conditional, name, &mut cache),
PreprocessorBranch::Unreadable
));
assert_eq!(
preprocessor_conditional_family_for_declaration_with_cache(name, &mut cache),
None,
"{needle}: an unreadable #elif branch does not complete the family"
);
assert_eq!(callable_guard_completion_byte(name, source), None);
}
let source = "namespace n {\n#if defined(A)\nBEGIN_NS\nint x;\nEND_NS\n#elif defined(B)\nBEGIN_NS\nclass T {};\nEND_NS\n#else\nBEGIN_NS\nclass T {};\nEND_NS\n#endif\n}\n";
let tree = parse(source);
let root = tree.root_node();
let start = source.rfind("class T").expect("else class") + "class ".len();
let name = root
.descendant_for_byte_range(start, start + 1)
.expect("else class name");
let elif = std::iter::successors(Some(name), |node| node.parent())
.find(|node| node.kind() == "preproc_elif")
.expect("structured elif");
assert_eq!(
cpp_displaced_preprocessor_branch(elif).map(|node| node.kind()),
Some("#else")
);
assert_eq!(
guards(name, source),
Some(set(&[
PreprocessorGuard::Defined("A".to_string()).negated(),
PreprocessorGuard::Defined("B".to_string()).negated(),
]))
);
let mut cache = PreprocessorContextCache::default();
assert!(
preprocessor_conditional_family_for_declaration_with_cache(name, &mut cache).is_some()
);
}
#[test]
#[hardwon(
"#2184: grammar 0.24.4 reads a macro-decorated return type cleanly, so the recovered-only same-file alias gate stopped matching"
)]
fn macro_modifier_decorated_type_matches_the_recovered_gate_scope() {
let source = "API static inline T make(void);\nAPI T value;\nvoid take(API T p);\nstatic inline T plain(void);\nAPI ns::T qualified(void);\nAPI static inline T body(void) { return 0; }\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let root = tree.root_node();
let type_before = |declared: &str| {
let end = source.find(declared).expect("declared name");
let start = source[..end].rfind('T').expect("type token");
root.descendant_for_byte_range(start, start + 1)
.expect("type node")
};
for declared in ["make(", "value;", "p)", "body("] {
let node = type_before(declared);
assert_eq!(node.kind(), "type_identifier", "{declared}");
assert!(
macro_modifier_decorated_type_node(node),
"{declared}: {}",
node.parent().expect("declaration").to_sexp()
);
}
for declared in ["plain(", "qualified("] {
let node = type_before(declared);
assert!(
!macro_modifier_decorated_type_node(node),
"{declared} is outside the macro-decorated head scope: {}",
node.parent().expect("parent").to_sexp()
);
}
}
#[test]
fn expression_defined_and_ifndef_guards_are_incompatible() {
let source = "#if defined(WIN_MODE)\nint selected;\n#endif\n#ifndef WIN_MODE\nint rejected;\n#endif\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let root = tree.root_node();
let selected_start = source.find("selected").expect("selected declaration");
let rejected_start = source.find("rejected").expect("rejected declaration");
let selected = root
.descendant_for_byte_range(selected_start, selected_start + "selected".len())
.expect("selected node");
let rejected = root
.descendant_for_byte_range(rejected_start, rejected_start + "rejected".len())
.expect("rejected node");
let selected_guards =
preprocessor_guard_environment(selected, source).expect("selected guards");
let rejected_guards =
preprocessor_guard_environment(rejected, source).expect("rejected guards");
assert!(
merge_preprocessor_guards(&selected_guards, &rejected_guards).is_none(),
"opposite spellings of one macro guard must contradict"
);
}
#[test]
#[hardwon("02b3765c2a06: C++ declaration recovery regressed for this structured syntax edge")]
fn split_language_linkage_wrapper_does_not_contradict_later_c_branch() {
let source = r#"#ifdef _WIN32
#if defined(__cplusplus)
extern "C"
#endif
int platform_api(void);
#endif
#ifdef _WIN32
static int entropy_target(void) { return 0; }
#else
#ifdef HAVE_COMMON_RANDOM
static int other_target(void) { return 0; }
#elif defined(HAVE_GETENTROPY)
static int entropy_target(void) { return 1; }
static int use_entropy(void) { return entropy_target(); }
#endif
#endif
"#;
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let start = source.rfind("entropy_target()").expect("reference");
let node = tree
.root_node()
.descendant_for_byte_range(start, start + "entropy_target".len())
.expect("reference node");
let guards = preprocessor_guard_environment(node, source).expect("active C branch");
assert!(
guards.contains(&PreprocessorGuard::Undefined("_WIN32".to_string())),
"{guards:#?}"
);
assert!(
guards.contains(&PreprocessorGuard::Undefined(
"HAVE_COMMON_RANDOM".to_string()
)),
"{guards:#?}"
);
assert!(
guards.contains(&PreprocessorGuard::Defined("HAVE_GETENTROPY".to_string())),
"{guards:#?}"
);
assert!(
!guards.contains(&PreprocessorGuard::Defined("_WIN32".to_string())),
"the malformed linkage wrapper must not impose its stale guard: {guards:#?}"
);
}
#[test]
#[hardwon("1e74d25492c1: C++ declaration recovery regressed for this structured syntax edge")]
fn ordinary_macro_role_distinguishes_conditional_body_from_directive_tokens() {
let source = "#define KEY 42\n#ifdef ENABLE_KEYS\nint classify(int value) {\n switch (value) {\n case KEY: return 1;\n default: return 0;\n }\n}\n#endif\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let root = tree.root_node();
let node_at = |text: &str, start: usize| {
root.descendant_for_byte_range(start, start + text.len())
.expect("token node")
};
let key_start = source.find("case KEY").expect("case label") + "case ".len();
let guard_start = source.find("ENABLE_KEYS").expect("guard name");
assert!(is_ordinary_macro_reference_node(node_at("KEY", key_start)));
assert!(!is_ordinary_macro_reference_node(node_at(
"ENABLE_KEYS",
guard_start,
)));
}
#[test]
#[hardwon("31913fee42a0: C++ declaration recovery regressed for this structured syntax edge")]
fn bare_macro_guard_is_implied_by_a_stronger_conjunction() {
let source = "#if HAVE_ARM_NEON\nstatic int target(void) { return 1; }\n#endif\n#if HAVE_ARM_NEON && ENABLE_FAST_PATH\nint use(void) { return target(); }\n#endif\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let root = tree.root_node();
let definition_start = source.find("target(void)").expect("definition");
let reference_start = source.rfind("target()").expect("reference");
let definition = root
.descendant_for_byte_range(definition_start, definition_start + "target".len())
.expect("definition node");
let reference = root
.descendant_for_byte_range(reference_start, reference_start + "target".len())
.expect("reference node");
let required =
preprocessor_guard_environment(definition, source).expect("definition guard");
let active = preprocessor_guard_environment(reference, source).expect("reference guard");
assert!(guard_requirements_hold_at_reference(
&required,
Some(&active)
));
}
#[test]
#[hardwon("31913fee42a0: C++ declaration recovery regressed for this structured syntax edge")]
fn g_autoptr_assignment_shape_recovers_only_the_named_macro_declarator() {
let source = "g_autoptr(FuChunkArray) self = make_array();";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let statement = tree.root_node().named_child(0).expect("statement");
let binding =
recognized_c_macro_declarator_binding(statement, source).expect("g_autoptr binding");
assert_eq!(binding.name, "self");
assert_eq!(binding.type_name, "FuChunkArray");
assert_eq!(binding.pointer_depth, 1);
let near_miss = "holder(FuChunkArray) self = make_array();";
let tree = parser.parse(near_miss, None).expect("near-miss tree");
let statement = tree.root_node().named_child(0).expect("statement");
assert!(recognized_c_macro_declarator_binding(statement, near_miss).is_none());
}
#[test]
#[hardwon("dc1b8c5a6ee8: C++ declaration recovery regressed for this structured syntax edge")]
fn boolean_guard_normalization_proves_equivalence_and_implication() {
let windows = BooleanGuardExpression::Defined("WIN32".to_string());
let cygwin = BooleanGuardExpression::Defined("CYGWIN".to_string());
let negated_windows_branch =
BooleanGuardExpression::all([windows.clone(), cygwin.negated()]).negated();
let portable = BooleanGuardExpression::any([windows.negated(), cygwin]);
assert_eq!(negated_windows_branch, portable);
let missing_a = BooleanGuardExpression::Undefined("A".to_string());
let missing_b = BooleanGuardExpression::Undefined("B".to_string());
let missing_c = BooleanGuardExpression::Undefined("C".to_string());
let fallback_branch = BooleanGuardExpression::any([missing_a.clone(), missing_b.clone()]);
let fallback_declaration = BooleanGuardExpression::any([missing_a, missing_b, missing_c]);
assert!(fallback_branch.implies(&fallback_declaration));
assert!(
BooleanGuardExpression::Truthy("FEATURE".to_string())
.implies(&BooleanGuardExpression::Defined("FEATURE".to_string()))
);
assert!(
BooleanGuardExpression::Undefined("FEATURE".to_string())
.implies(&BooleanGuardExpression::Falsy("FEATURE".to_string()))
);
assert!(
!BooleanGuardExpression::Defined("FEATURE".to_string())
.implies(&BooleanGuardExpression::Truthy("FEATURE".to_string()))
);
assert!(!fallback_declaration.implies(&fallback_branch));
}
#[test]
#[hardwon("15e5ef579cdd: C++ declaration recovery regressed for this structured syntax edge")]
fn c_keyword_argument_recovery_requires_an_enclosing_displaced_parameter() {
let source = "static int helper(const char *left, wchar_t *right) { return 0; }\nint caller(wchar_t *template) {\n if (1) {\n return helper(NULL, template); /* nested bound */\n }\n return helper(NULL, template); /* bound */\n}\nint lambda_scope(wchar_t *template) {\n return [=]() { return helper(NULL, template); }(); /* lambda scope */\n}\nint unbound(void) {\n return helper(NULL, template); /* unbound */\n}\n";
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("fixture tree");
let root = tree.root_node();
let call = |marker: &str| {
let start = source.find(marker).expect("call marker");
let mut node = root
.descendant_for_byte_range(start, start + "helper".len())
.expect("call name node");
loop {
if node.kind() == "call_expression" {
break node;
}
node = node.parent().expect("call expression ancestor");
}
};
let c_file = ProjectFile::new(std::env::temp_dir(), "keyword-argument.c");
let cpp_file = ProjectFile::new(std::env::temp_dir(), "keyword-argument.cpp");
let keyword_call = call("helper(NULL, template); /* bound */");
let keyword_arguments = keyword_call
.child_by_field_name("arguments")
.expect("keyword argument list");
assert_eq!(
recovered_c_keyword_argument_count(&c_file, keyword_call, keyword_arguments, source),
1
);
let nested_call = call("helper(NULL, template); /* nested bound */");
let nested_arguments = nested_call
.child_by_field_name("arguments")
.expect("nested keyword argument list");
assert_eq!(
recovered_c_keyword_argument_count(&c_file, nested_call, nested_arguments, source),
1
);
let lambda_call = call("helper(NULL, template); }(); /* lambda scope */");
let lambda_arguments = lambda_call
.child_by_field_name("arguments")
.expect("lambda keyword argument list");
assert_eq!(
recovered_c_keyword_argument_count(&c_file, lambda_call, lambda_arguments, source),
0,
"a lambda body must not borrow the enclosing collapsed C parameter"
);
assert_eq!(
recovered_c_keyword_argument_count(&cpp_file, keyword_call, keyword_arguments, source),
0
);
let unbound_call = call("helper(NULL, template); /* unbound */");
let unbound_arguments = unbound_call
.child_by_field_name("arguments")
.expect("unbound argument list");
assert_eq!(
recovered_c_keyword_argument_count(&c_file, unbound_call, unbound_arguments, source),
0
);
}
#[test]
#[brokk_bifrost_test_macros::hardwon(
"#4407: collapsed C keyword parameters need structural position proof"
)]
fn c_keyword_argument_recovery_rejects_non_parameter_keyword_positions() {
let source = r#"static int helper(const char *left, wchar_t *right) { return 0; }
int type_keyword(template *value) {
return helper(NULL, template); /* type */
}
int qualifier_keyword(const *template) {
return helper(NULL, template); /* qualifier */
}
int annotation_keyword(wchar_t *value __attribute__((template))) {
return helper(NULL, template); /* annotation */
}
int default_keyword(wchar_t *value = template) {
return helper(NULL, template); /* default */
}
int nested_expression(wchar_t *value, (template)) {
return helper(NULL, template); /* nested */
}
int malformed_head = (wchar_t *template) {
return helper(NULL, template); /* malformed */
};
"#;
let tree = parse_cpp(source);
let root = tree.root_node();
let call = |marker: &str| {
let start = source.find(marker).expect("call marker");
let mut node = root
.descendant_for_byte_range(start, start + "helper".len())
.expect("call name node");
loop {
if node.kind() == "call_expression" {
break Some(node);
}
let Some(parent) = node.parent() else {
break None;
};
node = parent;
}
};
let c_file = ProjectFile::new(std::env::temp_dir(), "keyword-near-miss.c");
let markers = [
("helper(NULL, template); /* type */", true),
("helper(NULL, template); /* qualifier */", false),
("helper(NULL, template); /* annotation */", false),
("helper(NULL, template); /* default */", false),
("helper(NULL, template); /* nested */", false),
("helper(NULL, template); /* malformed */", false),
];
let mut exercised = 0;
for (marker, expect_call) in markers {
let Some(keyword_call) = call(marker) else {
assert!(
!expect_call,
"{marker} lost its call_expression; the near-miss matrix changed"
);
continue;
};
assert!(
expect_call,
"{marker} unexpectedly retains a call_expression; the near-miss matrix changed"
);
exercised += 1;
let keyword_arguments = keyword_call
.child_by_field_name("arguments")
.expect("keyword argument list");
assert_eq!(
recovered_c_keyword_argument_count(
&c_file,
keyword_call,
keyword_arguments,
source,
),
0,
"keyword in {marker} must not prove a parameter binding"
);
}
assert!(
exercised > 0,
"at least one near-miss marker must exercise the count itself"
);
}
#[test]
#[hardwon("31982bd4d78a: C++ declaration recovery regressed for this structured syntax edge")]
fn c_function_declarator_recovery_accepts_invocations_not_binders() {
let source = r#"#define MAKE(type) type *value
MAKE(int *);
typedef struct Item Item;
struct CPUX86State { struct { int ZMM_L(int); } xmm_regs[8]; };
void gen_op_movl(void *s, int first, int second) { }
const char *strZ(const char *value) { return value; }
int body(void *s) {
MAKE(int *);
gen_op_movl(s, offsetof(CPUX86State, xmm_regs[0].ZMM_L(0)),
offsetof(CPUX86State, xmm_regs[0].ZMM_L(0)));
execvp(strZ(value), UNCONSTIFY(char **, args));
}
#define DEV_CHECK_PRESENCE(TYPE, MEMBER, DEVTYPE, PROPERTY, VALUE) \
if (!((TYPE)target)->MEMBER) { check(DEVTYPE, PROPERTY, VALUE); }
int recovered_deviation(struct Deviation *d, struct Target *target, void *ctx) {
if (d->units) {
switch (target->nodetype) {
case 1:
case 2:
break;
default:
AMEND_WRONG_NODETYPE("deviation", "replace", "units");
}
DEV_CHECK_PRESENCE(struct Item *, units, "replacing", "units", d->units);
lysdict_remove(ctx, ((struct Item *)target)->units);
DUP_STRING_GOTO(ctx, d->units, ((struct Item *)target)->units, ret, cleanup);
}
return 0;
}
STATIC EFI_STATUS Encode () { return 0; }
"#;
let tree = parse_cpp(source);
let top_macro_start = source.find("MAKE(int *);").expect("top macro");
let top_macro = tree
.root_node()
.named_descendant_for_byte_range(top_macro_start, top_macro_start + 4)
.expect("top macro node");
let body_macro_start = source
.match_indices("MAKE(int *);")
.nth(1)
.expect("body macro")
.0;
let body_macro = tree
.root_node()
.named_descendant_for_byte_range(body_macro_start, body_macro_start + 4)
.expect("body macro node");
let function_call_start = source
.find("gen_op_movl(s, offsetof(CPUX86State")
.expect("function call");
let function_call = tree
.root_node()
.named_descendant_for_byte_range(function_call_start, function_call_start + 11)
.expect("function call node");
let strz_start = source.find("strZ(value)").expect("nested function call");
let strz = tree
.root_node()
.named_descendant_for_byte_range(strz_start, strz_start + 4)
.expect("nested function call node");
let recovered_call_start = source.find("lysdict_remove(ctx").expect("recovered call");
let recovered_call = tree
.root_node()
.named_descendant_for_byte_range(
recovered_call_start,
recovered_call_start + "lysdict_remove".len(),
)
.expect("recovered call node");
let binder_start = source.find("Encode").expect("binder");
let binder = tree
.root_node()
.named_descendant_for_byte_range(binder_start, binder_start + 6)
.expect("binder node");
assert!(recovered_c_function_declarator_invocation(top_macro));
assert!(recovered_c_function_declarator_invocation(body_macro));
assert!(recovered_c_function_declarator_invocation(function_call));
assert!(recovered_c_function_declarator_invocation(strz));
assert!(recovered_c_function_declarator_invocation(recovered_call));
assert!(!recovered_c_function_declarator_invocation(binder));
}
#[test]
#[hardwon("31982bd4d78a: C++ declaration recovery regressed for this structured syntax edge")]
fn c_parenthesized_declarator_recovery_keeps_keyword_argument_and_rejects_siblings() {
let source = r#"typedef int krb5_context;
int helper(int first, int second) { return first + second; }
static krb5_context ctx;
int main(int argc, char **argv) {
int ccinitial;
const char *collection_name, *typename;
typename = helper(ctx, ccinitial);
return 0;
}
"#;
let tree = parse_cpp(source);
let ctx = tree
.root_node()
.descendant_for_byte_range(
source.find("ctx, ccinitial").expect("ctx argument"),
source.find("ctx, ccinitial").expect("ctx argument") + 3,
)
.expect("ctx node");
let ccinitial_start = source.find("ctx, ccinitial").expect("ctx argument") + 5;
let ccinitial = tree
.root_node()
.descendant_for_byte_range(ccinitial_start, ccinitial_start + "ccinitial".len())
.expect("sibling node");
let typename = named_node_at(&tree, source, "typename = helper");
let helper = named_node_at(&tree, source, "helper(ctx, ccinitial)");
assert_eq!(ctx.kind(), "type_identifier");
assert!(recovered_c_parenthesized_declarator_reference(ctx));
assert!(!recovered_c_parenthesized_declarator_reference(ccinitial));
assert!(!recovered_c_parenthesized_declarator_reference(typename));
assert!(!recovered_c_parenthesized_declarator_reference(helper));
}
fn first_enum_flattened_namespace(source: &str) -> Option<Vec<String>> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("C++ fixture tree");
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
if node.kind() == "enum_specifier" {
return flattened_macro_namespace_components(node, source);
}
let mut cursor = node.walk();
let children = node.named_children(&mut cursor).collect::<Vec<_>>();
stack.extend(children.into_iter().rev());
}
None
}
#[test]
#[hardwon("0e6df36dfa5d: C++ declaration recovery regressed for this structured syntax edge")]
fn flattened_namespace_scope_requires_a_complete_sentinel_envelope() {
let complete = r#"NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
enum class value_t { null };
}
NLOHMANN_JSON_NAMESPACE_END
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace next
{
struct next_type {};
}
NLOHMANN_JSON_NAMESPACE_END
"#;
assert_eq!(
first_enum_flattened_namespace(complete),
Some(vec!["detail".to_string()])
);
let stale_end = format!("NLOHMANN_JSON_NAMESPACE_END\n{complete}");
assert_eq!(
first_enum_flattened_namespace(&stale_end),
Some(vec!["detail".to_string()]),
"a stale end marker before the begin marker must not replace the intended namespace"
);
let incomplete = r#"NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
enum class value_t { null };
}
struct next_type {};
"#;
assert_eq!(first_enum_flattened_namespace(incomplete), None);
}
}
#[cfg(test)]
mod lookup_order_properties {
use super::*;
use proptest::prelude::*;
const ATOMS: [&str; 9] = ["a", "b", "A", "a$b", "a$", "$a", "ab", "naïve", "識別子"];
const REL_PATHS: [&str; 3] = ["a.cpp", "b.cpp", "sub/a.cpp"];
const ROOT_NAMES: [&str; 2] = ["ws", "ws_much_longer_root_name"];
const SIGNATURES: [Option<&str>; 3] = [None, Some("()"), Some("(int)")];
const KINDS: [CodeUnitType; 6] = [
CodeUnitType::Class,
CodeUnitType::Function,
CodeUnitType::Field,
CodeUnitType::Module,
CodeUnitType::Macro,
CodeUnitType::FileScope,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProbedOrder {
Before,
Tied,
After,
Contradictory,
}
impl ProbedOrder {
fn mirror(self) -> Self {
match self {
ProbedOrder::Before => ProbedOrder::After,
ProbedOrder::After => ProbedOrder::Before,
other => other,
}
}
fn signum(self) -> i8 {
match self {
ProbedOrder::Before => -1,
ProbedOrder::Tied => 0,
ProbedOrder::After => 1,
ProbedOrder::Contradictory => panic!("probed a non-dual comparator"),
}
}
}
fn probe_order(left: &CodeUnit, right: &CodeUnit) -> ProbedOrder {
if left == right {
return ProbedOrder::Tied;
}
let mut forward = vec![left.clone(), right.clone()];
sort_lookup_units(&mut forward);
let mut backward = vec![right.clone(), left.clone()];
sort_lookup_units(&mut backward);
let left_first = backward[0] == *left;
let right_first = forward[0] == *right;
match (left_first, right_first) {
(true, true) => ProbedOrder::Contradictory,
(true, false) => ProbedOrder::Before,
(false, true) => ProbedOrder::After,
(false, false) => ProbedOrder::Tied,
}
}
fn code_unit_strategy() -> impl Strategy<Value = CodeUnit> {
(
0..ROOT_NAMES.len(),
0..REL_PATHS.len(),
0..KINDS.len(),
prop::collection::vec((0..ATOMS.len(), 0..SegmentKind::ALL.len()), 1..=3),
0..3usize,
0..SIGNATURES.len(),
any::<bool>(),
)
.prop_map(
|(root, rel_path, kind, segments, package_prefix, signature, synthetic)| {
let source = ProjectFile::new(
std::env::temp_dir().join(ROOT_NAMES[root]),
REL_PATHS[rel_path],
);
let interner = segment_interner();
let mut fq = FqName::new();
for (atom, segment_kind) in &segments {
fq.push(interner.intern(ATOMS[*atom], SegmentKind::ALL[*segment_kind]));
}
let package_segment_count = package_prefix % fq.len();
CodeUnit::from_fq(
source,
KINDS[kind],
fq,
package_segment_count,
SIGNATURES[signature].map(str::to_string),
synthetic,
)
},
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
#[brokk_bifrost_test_macros::hardwon("#1876: distinct C++ declaration identities could tie in lookup ordering and expose hash iteration order")]
fn lookup_order_is_reflexive_and_dual(
left in code_unit_strategy(),
right in code_unit_strategy(),
) {
prop_assert_eq!(
probe_order(&left, &left),
ProbedOrder::Tied,
"a unit must tie with itself: {:?}",
left
);
let forward = probe_order(&left, &right);
prop_assert_ne!(
forward,
ProbedOrder::Contradictory,
"comparator put each of these strictly first: left={:?} right={:?}",
left,
right
);
prop_assert_eq!(
probe_order(&right, &left),
forward.mirror(),
"compare(b, a) must reverse compare(a, b): left={:?} right={:?}",
left,
right
);
}
#[test]
#[brokk_bifrost_test_macros::hardwon("#1876: distinct C++ declaration identities could tie in lookup ordering and expose hash iteration order")]
fn lookup_order_is_transitive(
a in code_unit_strategy(),
b in code_unit_strategy(),
c in code_unit_strategy(),
) {
let ab = probe_order(&a, &b);
let bc = probe_order(&b, &c);
let ac = probe_order(&a, &c);
for (probed, pair) in [(ab, "a,b"), (bc, "b,c"), (ac, "a,c")] {
prop_assert_ne!(
probed,
ProbedOrder::Contradictory,
"comparator is not dual over {}: a={:?} b={:?} c={:?}",
pair,
a,
b,
c
);
}
if ab.signum() <= 0 && bc.signum() <= 0 {
prop_assert!(
ac.signum() <= 0,
"transitivity broken: a<=b ({:?}) and b<=c ({:?}) but a?c is {:?}; \
a={:?} b={:?} c={:?}",
ab,
bc,
ac,
a,
b,
c
);
}
}
#[test]
#[brokk_bifrost_test_macros::hardwon("#1876: distinct C++ declaration identities could tie in lookup ordering and expose hash iteration order")]
fn lookup_order_separates_distinct_identities(
left in code_unit_strategy(),
right in code_unit_strategy(),
) {
if probe_order(&left, &right) == ProbedOrder::Tied {
prop_assert_eq!(
&left,
&right,
"distinct identities tied, so their order is whatever order they \
arrived in: left_segments={:?} right_segments={:?}",
left.fq_segments_debug(),
right.fq_segments_debug()
);
}
}
#[test]
#[brokk_bifrost_test_macros::hardwon("#1876: distinct C++ declaration identities could tie in lookup ordering and expose hash iteration order")]
fn lookup_sort_is_permutation_invariant(
units in prop::collection::vec(code_unit_strategy(), 1..=8),
) {
let mut sorted = units.clone();
sort_lookup_units(&mut sorted);
for rotation in 0..units.len() {
for reversed in [false, true] {
let mut permuted = units.clone();
permuted.rotate_left(rotation);
if reversed {
permuted.reverse();
}
sort_lookup_units(&mut permuted);
prop_assert_eq!(
&permuted,
&sorted,
"sorting a permutation gave a different list \
(rotation={}, reversed={}): input={:?}",
rotation,
reversed,
units
);
}
}
}
}
}
#[cfg(test)]
mod resolver_domain_properties {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn template_substitution_respects_free_variables(
first in 0u16..1000,
second in 1000u16..2000,
unrelated in prop::collection::vec(0u16..1000, 1..8),
rename in "[A-Z][a-z]{0,6}",
) {
let atom = |value: u16| CppTemplateTerm::Atom {
kind: "number_literal".to_owned(), text: value.to_string(),
};
let term = CppTemplateTerm::Node {
kind: "template_argument_list".to_owned(),
children: vec![CppTemplateTerm::Parameter("T".to_owned()), CppTemplateTerm::Parameter(format!("U{rename}"))],
};
let mut bindings = HashMap::default();
bindings.insert("T".to_owned(), atom(first));
prop_assert_eq!(cpp_substitute_template_term(&term, &bindings), None);
for (index, value) in unrelated.into_iter().enumerate() {
bindings.insert(format!("unrelated{index}"), atom(value));
prop_assert_eq!(cpp_substitute_template_term(&term, &bindings), None);
}
bindings.insert(format!("U{rename}"), atom(second));
let expected = CppTemplateTerm::Node {
kind: "template_argument_list".to_owned(), children: vec![atom(first), atom(second)],
};
prop_assert_eq!(cpp_substitute_template_term(&term, &bindings), Some(expected));
}
#[test]
fn callable_arity_merge_is_a_symmetric_domain_union(
total in 0usize..20,
left_required in 0usize..20,
right_required in 0usize..20,
left_variadic in any::<bool>(),
right_variadic in any::<bool>(),
) {
let left = CallableArity::new(left_required.min(total), total, left_variadic);
let right = CallableArity::new(right_required.min(total), total, right_variadic);
let merged = merge_compatible_callable_arities(left, right);
prop_assert_eq!(merged, merge_compatible_callable_arities(right, left));
if let Some(merged) = merged {
for count in 0..=total + 2 {
prop_assert_eq!(merged.accepts(count), left.accepts(count) || right.accepts(count));
}
} else {
prop_assert_ne!(left_variadic, right_variadic);
}
}
#[test]
fn guard_environment_conjunction_is_idempotent(names in prop::collection::vec("[A-Z]{1,5}", 1..8)) {
let guards: HashSet<_> = names.into_iter().map(PreprocessorGuard::Defined).collect();
let environment = PreprocessorGuardEnvironment { all: guards.clone(), partial: guards.clone() };
let twice = environment.merged(&environment).unwrap();
prop_assert_eq!(&twice.all, &guards);
prop_assert_eq!(&twice.partial, &guards);
}
}
}
#[cfg(test)]
mod resolver_lookup_key_properties {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn parsed_nested_types_round_trip_through_bounded_lookup_keys(
namespaces in 0usize..3,
owners in 1usize..5,
suffix in "[a-z]{1,6}",
) {
let mut source = String::new();
let mut components = Vec::new();
for index in 0..namespaces {
let name = format!("ns{index}_{suffix}");
source.push_str(&format!("namespace {name} {{ "));
components.push(name);
}
for index in 0..owners {
let name = format!("Owner{index}");
source.push_str(&format!("struct {name} {{ "));
components.push(name);
}
source.push_str("struct Leaf {}; ");
for _ in 0..owners { source.push_str("}; "); }
for _ in 0..namespaces { source.push_str("} "); }
components.push("Leaf".to_owned());
source.push_str(&format!("{}* value;", components.join("::")));
let tree = crate::diagnostics::parse_cpp_tree(&source);
prop_assert!(!tree.root_node().has_error());
let file = ProjectFile::new(std::env::temp_dir(), "lookup-keys.cpp");
let parsed = crate::adapter::parse_cpp_file(&file, &source, &tree);
let leaf = parsed.declarations().iter()
.find(|unit| unit.is_class() && unit.identifier() == "Leaf")
.expect("declaration producer indexed the nested type");
let declaration = tree.root_node().named_child(tree.root_node().named_child_count() - 1).unwrap();
prop_assert_eq!(declaration.kind(), "declaration");
let type_node = declaration.child_by_field_name("type").unwrap();
let parts = cpp_type_name_components(type_node, &source).unwrap();
prop_assert_eq!(&parts, &components);
let candidates = cpp_reference_fqn_candidates_from_parts(&parts, TargetKind::Type);
let declaration_key = leaf.fq_name();
prop_assert!(candidates.contains(&declaration_key), "declaration key {:?} absent from bounded query keys {:?}", declaration_key, candidates);
}
}
}