Skip to main content

brokk_bifrost_cpp/graph/
syntax.rs

1use crate::graph::resolver::{
2    cpp_name_component_nodes, cpp_type_name_components, is_globally_qualified_cpp_name,
3    is_nested_type_node, qualified_owner_components,
4};
5use std::ops::Range;
6use tree_sitter::{Node, Parser};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct MacroReplacementTypeReference {
10    pub components: Vec<String>,
11    pub component_ranges: Vec<Range<usize>>,
12    pub global: bool,
13}
14
15/// Recover type-bearing syntax hidden inside an object-like macro replacement.
16///
17/// Tree-sitter deliberately keeps the replacement of `#define NAME value` as
18/// one opaque `preproc_arg`. Reparse that exact byte slice as a C++ expression
19/// and return only references proven by the resulting tree: ordinary type
20/// nodes and the owner prefixes of qualified values such as `Owner::member`.
21/// Every returned range is mapped back to the original file.
22pub fn object_macro_replacement_type_references(
23    node: Node<'_>,
24    source: &str,
25) -> Vec<MacroReplacementTypeReference> {
26    if node.kind() != "preproc_arg"
27        || !node.parent().is_some_and(|parent| {
28            parent.kind() == "preproc_def"
29                && parent
30                    .child_by_field_name("value")
31                    .is_some_and(|value| value == node)
32        })
33    {
34        return Vec::new();
35    }
36    let Some(replacement) = source.get(node.start_byte()..node.end_byte()) else {
37        return Vec::new();
38    };
39    const PREFIX: &str = "void __bifrost_macro_reference() { ";
40    let synthetic = format!("{PREFIX}{replacement}; }}");
41    let mut parser = Parser::new();
42    if parser
43        .set_language(&tree_sitter_cpp::LANGUAGE.into())
44        .is_err()
45    {
46        return Vec::new();
47    }
48    let Some(tree) = parser.parse(&synthetic, None) else {
49        return Vec::new();
50    };
51    if tree.root_node().has_error() {
52        return Vec::new();
53    }
54
55    let mut references = Vec::new();
56    let mut stack = vec![tree.root_node()];
57    while let Some(current) = stack.pop() {
58        let structured = if matches!(
59            current.kind(),
60            "type_identifier" | "scoped_type_identifier" | "template_type"
61        ) && !is_nested_type_node(current)
62        {
63            cpp_type_name_components(current, &synthetic)
64                .zip(cpp_name_component_nodes(current))
65                .map(|(components, nodes)| {
66                    (components, nodes, is_globally_qualified_cpp_name(current))
67                })
68        } else if current.kind() == "qualified_identifier"
69            && !current.parent().is_some_and(|parent| {
70                matches!(
71                    parent.kind(),
72                    "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier"
73                )
74            })
75        {
76            qualified_owner_components(current, &synthetic)
77                .map(|owner| (owner.names, owner.nodes, owner.global))
78        } else {
79            None
80        };
81        if let Some((components, component_nodes, global)) = structured {
82            let component_ranges = component_nodes
83                .into_iter()
84                .map(|component| {
85                    let start = component.start_byte().checked_sub(PREFIX.len())?;
86                    let end = component.end_byte().checked_sub(PREFIX.len())?;
87                    (end <= replacement.len())
88                        .then_some(node.start_byte() + start..node.start_byte() + end)
89                })
90                .collect::<Option<Vec<_>>>();
91            if let Some(component_ranges) = component_ranges
92                && component_ranges.len() == components.len()
93            {
94                let reference = MacroReplacementTypeReference {
95                    components,
96                    component_ranges,
97                    global,
98                };
99                if !references.contains(&reference) {
100                    references.push(reference);
101                }
102            }
103        }
104        for index in (0..current.named_child_count()).rev() {
105            if let Some(child) = current.named_child(index) {
106                stack.push(child);
107            }
108        }
109    }
110    references
111}
112
113#[derive(Clone)]
114pub struct QualifiedCallableValue<'tree> {
115    pub qualified: Node<'tree>,
116    pub global: bool,
117    pub owner_components: Vec<Node<'tree>>,
118    pub member: Node<'tree>,
119}
120
121/// Recognize an explicit address-of qualified callable value such as
122/// `&Owner::method` or `&namespace::Owner::method`.
123///
124/// The returned nodes come exclusively from the C++ grammar's named fields. In
125/// particular, a nested namespace/type owner remains a structured subtree rather
126/// than being reconstructed from source text.
127pub fn explicit_qualified_callable_value(node: Node<'_>) -> Option<QualifiedCallableValue<'_>> {
128    if node.kind() != "pointer_expression" || node.child_by_field_name("operator")?.kind() != "&" {
129        return None;
130    }
131    let qualified = node.child_by_field_name("argument")?;
132    qualified_callable_value_from_node(qualified)
133}
134
135/// Recognize a qualified callable used as an expression value.
136///
137/// Calls use their own arity-aware path. Address-of expressions use the
138/// explicit path above. This arm covers structured values such as
139/// `bind(Owner::method)` and `callback = namespace::function`.
140pub fn qualified_callable_value(node: Node<'_>) -> Option<QualifiedCallableValue<'_>> {
141    if let Some(value) = explicit_qualified_callable_value(node) {
142        return Some(value);
143    }
144    if node.kind() != "qualified_identifier" {
145        return None;
146    }
147    if crate::graph::resolver::is_declaration_name(node) {
148        return None;
149    }
150    if node.parent().is_some_and(|parent| {
151        parent.child_by_field_name("type") == Some(node)
152            || (parent.kind() == "call_expression"
153                && parent.child_by_field_name("function") == Some(node))
154            || (parent.kind() == "pointer_expression"
155                && parent.child_by_field_name("argument") == Some(node))
156            || matches!(
157                parent.kind(),
158                "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier"
159            )
160    }) {
161        return None;
162    }
163    qualified_callable_value_from_node(node)
164}
165
166fn qualified_callable_value_from_node(qualified: Node<'_>) -> Option<QualifiedCallableValue<'_>> {
167    if qualified.kind() != "qualified_identifier" {
168        return None;
169    }
170    let mut components = Vec::new();
171    let global = qualified.child_by_field_name("scope").is_none()
172        && qualified.child(0).is_some_and(|child| child.kind() == "::");
173    append_qualified_components(qualified, &mut components)?;
174    let member = components.pop()?;
175    if components.is_empty() {
176        return None;
177    }
178    Some(QualifiedCallableValue {
179        qualified,
180        global,
181        owner_components: components,
182        member,
183    })
184}
185
186fn append_qualified_components<'tree>(node: Node<'tree>, out: &mut Vec<Node<'tree>>) -> Option<()> {
187    let mut stack = vec![node];
188    while let Some(current) = stack.pop() {
189        match current.kind() {
190            "identifier" | "namespace_identifier" | "type_identifier" | "operator_name" => {
191                out.push(current)
192            }
193            "qualified_identifier" | "scoped_identifier" => {
194                stack.push(current.child_by_field_name("name")?);
195                if let Some(scope) = current.child_by_field_name("scope") {
196                    stack.push(scope);
197                } else if current.child(0).is_none_or(|child| child.kind() != "::") {
198                    return None;
199                }
200            }
201            "template_type" | "template_function" => {
202                stack.push(current.child_by_field_name("name")?);
203            }
204            "nested_namespace_specifier" => {
205                for index in (0..current.named_child_count()).rev() {
206                    stack.push(current.named_child(index)?);
207                }
208            }
209            _ => return None,
210        }
211    }
212    Some(())
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    fn references(source: &str) -> Vec<MacroReplacementTypeReference> {
220        let mut parser = Parser::new();
221        parser
222            .set_language(&tree_sitter_cpp::LANGUAGE.into())
223            .expect("C++ grammar");
224        let tree = parser.parse(source, None).expect("macro fixture tree");
225        let value = tree
226            .root_node()
227            .named_child(0)
228            .and_then(|definition| definition.child_by_field_name("value"))
229            .expect("macro replacement");
230        object_macro_replacement_type_references(value, source)
231    }
232
233    #[test]
234    fn object_macro_replacement_reparse_preserves_type_ranges() {
235        let source = "#define SETTINGS (*api::SettingsImpl::GetInstance())\n";
236        let references = references(source);
237        let reference = references
238            .iter()
239            .find(|reference| reference.components == ["api", "SettingsImpl"])
240            .expect("qualified callable owner");
241        let rendered = reference
242            .component_ranges
243            .iter()
244            .map(|range| &source[range.clone()])
245            .collect::<Vec<_>>();
246        assert_eq!(rendered, ["api", "SettingsImpl"]);
247    }
248
249    #[test]
250    fn macro_reparse_ignores_function_like_and_non_code_text() {
251        let function_like = "#define SETTINGS(Type) (*Type::GetInstance())\n";
252        assert!(references(function_like).is_empty());
253
254        let text = "#define SETTINGS \"SettingsImpl::GetInstance()\"\n";
255        assert!(references(text).is_empty());
256    }
257}