Skip to main content

macho_header_syntax/
parse.rs

1//! Tree-sitter-backed header parsing and typed lowering.
2
3mod declaration;
4mod diagnostics;
5mod type_parse;
6
7use declaration::{lower_declaration, lower_record};
8pub(super) use type_parse::parse_type;
9
10use tree_sitter::{Node, Parser};
11
12use crate::{
13    Decl, FunctionQualifiers, Identifier, IdentifierPath, Language, Linkage, MethodKind,
14    NamedTypeTag, ObjectiveCForwardKind, ObjectiveCMethod, Parameter, ParameterState, RecordKind,
15    StorageClass, TranslationUnit,
16};
17
18/// A source span reported by the parser.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct SourceSpan {
21    /// Zero-based starting byte offset.
22    pub start: usize,
23    /// Exclusive ending byte offset.
24    pub end: usize,
25    /// One-based line number.
26    pub line: usize,
27    /// One-based column number.
28    pub column: usize,
29}
30
31/// A concrete syntax issue.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SyntaxIssue {
34    /// Tree-sitter node kind.
35    pub kind: String,
36    /// Source location.
37    pub span: SourceSpan,
38}
39
40/// Header parsing failure.
41#[derive(Debug, thiserror::Error)]
42pub enum ParseError {
43    /// The selected grammar could not be installed.
44    #[error("failed to initialize {0:?} header grammar")]
45    Grammar(Language),
46    /// Tree-sitter could not produce a tree.
47    #[error("header parser did not produce a syntax tree")]
48    NoTree,
49    /// Concrete syntax errors were found.
50    #[error("header contains syntax errors: {}", diagnostics::format_syntax_issues(.0))]
51    Syntax(Vec<SyntaxIssue>),
52    /// A syntactically valid construct cannot be represented by the typed AST.
53    #[error("unsupported header construct `{kind}` at byte {span_start}")]
54    Unsupported {
55        /// Tree-sitter construct name.
56        kind: String,
57        /// Starting byte offset.
58        span_start: usize,
59    },
60    /// A declaration was syntactically present but not structurally valid.
61    #[error("invalid declaration: {0}")]
62    InvalidDeclaration(String),
63}
64
65/// Process-free parser for C-family headers.
66pub trait HeaderParser {
67    /// Parses a complete header translation unit.
68    fn parse(&self, language: Language, source: &str) -> Result<TranslationUnit, ParseError>;
69}
70
71/// Tree-sitter implementation of [`HeaderParser`].
72#[derive(Debug, Default, Clone, Copy)]
73pub struct TreeSitterHeaderParser;
74
75impl HeaderParser for TreeSitterHeaderParser {
76    fn parse(&self, language: Language, source: &str) -> Result<TranslationUnit, ParseError> {
77        let mut parser = Parser::new();
78        let grammar = match language {
79            Language::C => tree_sitter_c::LANGUAGE,
80            Language::Cpp => tree_sitter_cpp::LANGUAGE,
81            Language::ObjectiveC => tree_sitter_objc::LANGUAGE,
82        };
83        parser
84            .set_language(&grammar.into())
85            .map_err(|_| ParseError::Grammar(language))?;
86        let tree = parser.parse(source, None).ok_or(ParseError::NoTree)?;
87        let root = tree.root_node();
88        if root.has_error() {
89            let mut issues = Vec::new();
90            collect_syntax_issues(root, &mut issues);
91            return Err(ParseError::Syntax(issues));
92        }
93
94        let mut declarations = Vec::new();
95        let mut declaration_spans = Vec::new();
96        lower_children(
97            root,
98            source,
99            language,
100            &mut declarations,
101            &mut declaration_spans,
102        )?;
103        Ok(TranslationUnit {
104            language,
105            declarations,
106            declaration_spans,
107        })
108    }
109}
110
111fn collect_syntax_issues(node: Node<'_>, issues: &mut Vec<SyntaxIssue>) {
112    if node.is_error() || node.is_missing() {
113        let point = node.start_position();
114        issues.push(SyntaxIssue {
115            kind: if node.is_missing() {
116                format!("missing {}", node.kind())
117            } else {
118                node.kind().to_owned()
119            },
120            span: SourceSpan {
121                start: node.start_byte(),
122                end: node.end_byte(),
123                line: point.row + 1,
124                column: point.column + 1,
125            },
126        });
127        return;
128    }
129    let mut cursor = node.walk();
130    for child in node.children(&mut cursor) {
131        collect_syntax_issues(child, issues);
132    }
133}
134
135fn lower_children(
136    node: Node<'_>,
137    source: &str,
138    language: Language,
139    declarations: &mut Vec<Decl>,
140    declaration_spans: &mut Vec<SourceSpan>,
141) -> Result<(), ParseError> {
142    let mut cursor = node.walk();
143    for child in node.named_children(&mut cursor) {
144        let text = child
145            .utf8_text(source.as_bytes())
146            .map_err(|_| ParseError::InvalidDeclaration("header is not valid UTF-8".to_owned()))?;
147        match child.kind() {
148            "comment"
149            | "preproc_include"
150            | "preproc_def"
151            | "preproc_function_def"
152            | "preproc_call"
153            | "preproc_if"
154            | "preproc_ifdef" => {}
155            "declaration" | "type_definition" | "alias_declaration" => {
156                extend_declarations(
157                    declarations,
158                    declaration_spans,
159                    lower_declaration(text, language)?,
160                    source_span(child),
161                );
162            }
163            "struct_specifier" | "union_specifier" | "class_specifier" | "enum_specifier" => {
164                declarations.push(lower_record(text, language)?);
165                declaration_spans.push(source_span(child));
166            }
167            "namespace_definition" => {
168                // Namespace ownership is represented on lowered declaration paths/report
169                // owners, not as a source-text node.  Only the declaration body is a
170                // declaration; the grammar's `namespace_identifier` is metadata.
171                let body = child
172                    .child_by_field_name("body")
173                    .or_else(|| named_child_of_kind(child, "declaration_list"))
174                    .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
175                lower_children(body, source, language, declarations, declaration_spans)?;
176            }
177            "linkage_specification" => {
178                // Ignore the grammar's string literal and lower the declaration/body.
179                let mut nested = child.walk();
180                for declaration in child
181                    .named_children(&mut nested)
182                    .filter(|node| matches!(node.kind(), "declaration" | "declaration_list"))
183                {
184                    if declaration.kind() == "declaration_list" {
185                        lower_children(
186                            declaration,
187                            source,
188                            language,
189                            declarations,
190                            declaration_spans,
191                        )?;
192                    } else {
193                        let text = declaration.utf8_text(source.as_bytes()).map_err(|_| {
194                            ParseError::InvalidDeclaration("header is not valid UTF-8".to_owned())
195                        })?;
196                        extend_declarations(
197                            declarations,
198                            declaration_spans,
199                            lower_declaration(text, language)?,
200                            source_span(declaration),
201                        );
202                    }
203                }
204            }
205            "declaration_list" => {
206                lower_children(child, source, language, declarations, declaration_spans)?
207            }
208            "class_interface" => {
209                declarations.push(lower_objc_interface(text)?);
210                declaration_spans.push(source_span(child));
211            }
212            "protocol_declaration" | "qualified_protocol_interface_declaration" => {
213                declarations.push(lower_objc_protocol(text)?);
214                declaration_spans.push(source_span(child));
215            }
216            "class_forward_declaration" | "class_declaration" => {
217                declarations.push(lower_objc_forward(text, ObjectiveCForwardKind::Class)?);
218                declaration_spans.push(source_span(child));
219            }
220            "protocol_forward_declaration" | "protocol_forward_declaration_list" => {
221                declarations.push(lower_objc_forward(text, ObjectiveCForwardKind::Protocol)?);
222                declaration_spans.push(source_span(child));
223            }
224            // Empty declarations and compiler pragmas carry no recoverable declaration.
225            ";" | "preproc_directive" => {}
226            kind => {
227                return Err(ParseError::Unsupported {
228                    kind: kind.to_owned(),
229                    span_start: child.start_byte(),
230                });
231            }
232        }
233    }
234    Ok(())
235}
236
237fn extend_declarations(
238    declarations: &mut Vec<Decl>,
239    spans: &mut Vec<SourceSpan>,
240    lowered: Vec<Decl>,
241    span: SourceSpan,
242) {
243    spans.extend(std::iter::repeat_n(span, lowered.len()));
244    declarations.extend(lowered);
245}
246
247fn source_span(node: Node<'_>) -> SourceSpan {
248    let point = node.start_position();
249    SourceSpan {
250        start: node.start_byte(),
251        end: node.end_byte(),
252        line: point.row + 1,
253        column: point.column + 1,
254    }
255}
256
257fn named_child_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
258    let mut cursor = node.walk();
259    node.named_children(&mut cursor)
260        .find(|child| child.kind() == kind)
261}
262
263fn lower_objc_interface(text: &str) -> Result<Decl, ParseError> {
264    let header = text
265        .lines()
266        .next()
267        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?
268        .trim()
269        .trim_start_matches("@interface")
270        .trim();
271    let head = header.split('{').next().unwrap_or(header).trim();
272    let (before_protocols, protocols) = parse_objc_protocols(head)?;
273    if let Some(open) = before_protocols.find('(') {
274        let close = before_protocols[open + 1..]
275            .find(')')
276            .map(|offset| open + 1 + offset)
277            .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
278        let extended_class = parse_identifier(before_protocols[..open].trim())?;
279        let name = parse_identifier(before_protocols[open + 1..close].trim())?;
280        let (methods, properties) = parse_objc_members(text)?;
281        return Ok(Decl::ObjectiveCCategory {
282            name,
283            extended_class,
284            protocols,
285            methods,
286            properties,
287        });
288    }
289    let (name, superclass) = if let Some((name, superclass)) = before_protocols.split_once(':') {
290        (
291            parse_identifier(name.trim())?,
292            Some(parse_identifier(superclass.trim())?),
293        )
294    } else {
295        (parse_identifier(before_protocols.trim())?, None)
296    };
297    let (methods, properties) = parse_objc_members(text)?;
298    Ok(Decl::ObjectiveCInterface {
299        name,
300        superclass,
301        protocols,
302        ivars: parse_objc_ivars(text)?,
303        methods,
304        properties,
305    })
306}
307
308fn lower_objc_protocol(text: &str) -> Result<Decl, ParseError> {
309    let header = text
310        .lines()
311        .next()
312        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?
313        .trim()
314        .trim_start_matches("@protocol")
315        .trim();
316    let (name, protocols) = parse_objc_protocols(header)?;
317    let (methods, properties) = parse_objc_members(text)?;
318    Ok(Decl::ObjectiveCProtocol {
319        name: parse_identifier(name.trim())?,
320        protocols,
321        methods,
322        properties,
323    })
324}
325
326fn lower_objc_forward(text: &str, kind: ObjectiveCForwardKind) -> Result<Decl, ParseError> {
327    let keyword = match kind {
328        ObjectiveCForwardKind::Class => "@class",
329        ObjectiveCForwardKind::Protocol => "@protocol",
330    };
331    let names = text
332        .trim()
333        .trim_start_matches(keyword)
334        .trim_end_matches(';')
335        .split(',')
336        .map(str::trim)
337        .map(parse_identifier)
338        .collect::<Result<Vec<_>, _>>()?;
339    if names.is_empty() {
340        return Err(ParseError::InvalidDeclaration(text.to_owned()));
341    }
342    Ok(Decl::ObjectiveCForward { kind, names })
343}
344
345fn parse_objc_protocols(text: &str) -> Result<(&str, Vec<Identifier>), ParseError> {
346    let Some(open) = text.find('<') else {
347        return Ok((text, Vec::new()));
348    };
349    let close = text[open + 1..]
350        .find('>')
351        .map(|offset| open + 1 + offset)
352        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
353    let protocols = text[open + 1..close]
354        .split(',')
355        .map(str::trim)
356        .map(parse_identifier)
357        .collect::<Result<Vec<_>, _>>()?;
358    Ok((text[..open].trim(), protocols))
359}
360
361fn parse_objc_members(
362    text: &str,
363) -> Result<(Vec<ObjectiveCMethod>, Vec<crate::ObjectiveCProperty>), ParseError> {
364    let mut without_ivars = text.to_owned();
365    if let Some(open) = without_ivars.find('{')
366        && let Some(close) = matching_delimiter(&without_ivars, open, '{', '}')
367    {
368        without_ivars.replace_range(open..=close, "");
369    }
370    let mut body = without_ivars.lines().skip(1).collect::<Vec<_>>().join("\n");
371    body = body.replace("@end", "");
372    let mut required = None;
373    let mut methods = Vec::new();
374    let mut properties = Vec::new();
375    for raw in split_top_level(&body, ';') {
376        let mut value = raw.trim();
377        for (directive, state) in [("@required", Some(true)), ("@optional", Some(false))] {
378            if let Some(rest) = value.strip_prefix(directive) {
379                required = state;
380                value = rest.trim();
381            }
382        }
383        if value.starts_with('-') || value.starts_with('+') {
384            let mut method = parse_objc_method(value)?;
385            method.required = required;
386            methods.push(method);
387        } else if value.starts_with("@property") {
388            properties.push(parse_objc_property(value)?);
389        }
390    }
391    Ok((methods, properties))
392}
393
394fn parse_objc_ivars(text: &str) -> Result<Vec<crate::ObjectiveCIvar>, ParseError> {
395    let Some(open) = text.find('{') else {
396        return Ok(Vec::new());
397    };
398    let close = matching_delimiter(text, open, '{', '}')
399        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
400    let mut access = crate::ObjectiveCAccess::Protected;
401    let mut ivars = Vec::new();
402    for raw in split_top_level(&text[open + 1..close], ';') {
403        let mut value = raw.trim();
404        for (directive, next) in [
405            ("@public", crate::ObjectiveCAccess::Public),
406            ("@protected", crate::ObjectiveCAccess::Protected),
407            ("@private", crate::ObjectiveCAccess::Private),
408            ("@package", crate::ObjectiveCAccess::Package),
409        ] {
410            if let Some(rest) = value.strip_prefix(directive) {
411                access = next;
412                value = rest.trim();
413            }
414        }
415        if value.is_empty() {
416            continue;
417        }
418        let (ty, name) = split_type_and_name(value)?;
419        ivars.push(crate::ObjectiveCIvar {
420            name: parse_identifier(name.trim_start_matches('*').trim())?,
421            ty: parse_type(ty, Language::ObjectiveC)?,
422            access,
423        });
424    }
425    Ok(ivars)
426}
427
428fn parse_objc_property(text: &str) -> Result<crate::ObjectiveCProperty, ParseError> {
429    let mut value = text.trim_start_matches("@property").trim();
430    let mut attributes = Vec::new();
431    if value.starts_with('(') {
432        let close = matching_delimiter(value, 0, '(', ')')
433            .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
434        attributes = value[1..close]
435            .split(',')
436            .map(str::trim)
437            .map(parse_objc_property_attribute)
438            .collect::<Result<Vec<_>, _>>()?;
439        value = value[close + 1..].trim();
440    }
441    let (ty, name) = split_type_and_name(value)?;
442    Ok(crate::ObjectiveCProperty {
443        name: parse_identifier(name.trim_start_matches('*').trim())?,
444        ty: parse_type(ty, Language::ObjectiveC)?,
445        attributes,
446    })
447}
448
449fn parse_objc_property_attribute(
450    value: &str,
451) -> Result<crate::ObjectiveCPropertyAttribute, ParseError> {
452    use crate::ObjectiveCPropertyAttribute as Attribute;
453    match value {
454        "readonly" => Ok(Attribute::Readonly),
455        "readwrite" => Ok(Attribute::Readwrite),
456        "copy" => Ok(Attribute::Copy),
457        "retain" => Ok(Attribute::Retain),
458        "strong" => Ok(Attribute::Strong),
459        "weak" => Ok(Attribute::Weak),
460        "assign" => Ok(Attribute::Assign),
461        "atomic" => Ok(Attribute::Atomic),
462        "nonatomic" => Ok(Attribute::Nonatomic),
463        "dynamic" => Ok(Attribute::Dynamic),
464        "class" => Ok(Attribute::Class),
465        _ => Err(ParseError::InvalidDeclaration(format!(
466            "unknown Objective-C property attribute `{value}`"
467        ))),
468    }
469}
470
471fn parse_objc_method(text: &str) -> Result<ObjectiveCMethod, ParseError> {
472    let kind = if text.starts_with('+') {
473        MethodKind::Class
474    } else {
475        MethodKind::Instance
476    };
477    let rest = text[1..].trim();
478    let return_open = rest
479        .find('(')
480        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
481    let return_close = matching_delimiter(rest, return_open, '(', ')')
482        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
483    let return_type = parse_type(&rest[return_open + 1..return_close], Language::ObjectiveC)?;
484    let tail = rest[return_close + 1..].trim();
485    if !tail.contains(':') {
486        return Ok(ObjectiveCMethod {
487            kind,
488            selector: tail.to_owned(),
489            return_type,
490            parameters: Vec::new(),
491            required: None,
492        });
493    }
494    let mut selector = String::new();
495    let mut parameters = Vec::new();
496    let mut remaining = tail;
497    let mut index = 0usize;
498    while let Some(colon) = remaining.find(':') {
499        let piece = remaining[..colon]
500            .split_whitespace()
501            .last()
502            .unwrap_or_default();
503        selector.push_str(piece);
504        selector.push(':');
505        remaining = remaining[colon + 1..].trim_start();
506        let open = remaining
507            .find('(')
508            .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
509        let close = matching_delimiter(remaining, open, '(', ')')
510            .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
511        let ty = parse_type(&remaining[open + 1..close], Language::ObjectiveC)?;
512        remaining = remaining[close + 1..].trim_start();
513        let end = remaining
514            .find(char::is_whitespace)
515            .unwrap_or(remaining.len());
516        let candidate = &remaining[..end];
517        let name = Identifier::new(candidate).unwrap_or_else(|| {
518            Identifier::new(format!("arg{index}")).expect("generated identifier is valid")
519        });
520        index += 1;
521        parameters.push(Parameter { name, ty });
522        remaining = remaining[end..].trim_start();
523    }
524    Ok(ObjectiveCMethod {
525        kind,
526        selector,
527        return_type,
528        parameters,
529        required: None,
530    })
531}
532
533pub(super) fn parse_parameters(
534    text: &str,
535    language: Language,
536) -> Result<(Vec<Parameter>, bool, ParameterState), ParseError> {
537    let text = text.trim();
538    if text.is_empty() {
539        return Ok((Vec::new(), false, ParameterState::Unspecified));
540    }
541    if text == "void" {
542        return Ok((Vec::new(), false, ParameterState::Known));
543    }
544    let mut parameters = Vec::new();
545    let mut variadic = false;
546    for (index, parameter) in split_top_level(text, ',').into_iter().enumerate() {
547        let parameter = parameter.trim();
548        if parameter == "..." {
549            variadic = true;
550            continue;
551        }
552        let (ty, name) = split_type_and_name(parameter).unwrap_or((parameter, ""));
553        let name = Identifier::new(name.trim_start_matches('*').trim()).unwrap_or_else(|| {
554            Identifier::new(format!("arg{index}")).expect("generated identifier is valid")
555        });
556        parameters.push(Parameter {
557            name,
558            ty: parse_type(ty, language)?,
559        });
560    }
561    Ok((parameters, variadic, ParameterState::Known))
562}
563
564pub(super) fn split_type_and_declarators(text: &str) -> Result<(&str, Vec<&str>), ParseError> {
565    let parts = split_top_level(text, ',');
566    let first = parts
567        .first()
568        .copied()
569        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
570    let (ty, first_name) = split_type_and_name(first)?;
571    let mut names = vec![first_name];
572    names.extend(parts.into_iter().skip(1));
573    Ok((ty, names))
574}
575
576pub(super) fn split_type_and_name(text: &str) -> Result<(&str, &str), ParseError> {
577    let text = text.trim();
578    let end = text
579        .char_indices()
580        .rev()
581        .find(|(_, ch)| ch.is_ascii_alphanumeric() || *ch == '_')
582        .map(|(index, ch)| index + ch.len_utf8())
583        .ok_or_else(|| ParseError::InvalidDeclaration(text.to_owned()))?;
584    let start = text[..end]
585        .char_indices()
586        .rev()
587        .take_while(|(_, ch)| ch.is_ascii_alphanumeric() || *ch == '_')
588        .last()
589        .map(|(index, _)| index)
590        .unwrap_or(0);
591    let name = &text[start..end];
592    let ty = text[..start].trim();
593    if ty.is_empty() || Identifier::new(name).is_none() {
594        return Err(ParseError::InvalidDeclaration(text.to_owned()));
595    }
596    Ok((ty, name))
597}
598
599pub(super) fn parse_identifier(text: &str) -> Result<Identifier, ParseError> {
600    Identifier::new(text.trim())
601        .ok_or_else(|| ParseError::InvalidDeclaration(format!("invalid identifier `{text}`")))
602}
603
604pub(super) fn parse_path(text: &str) -> Result<IdentifierPath, ParseError> {
605    IdentifierPath::parse(text)
606        .ok_or_else(|| ParseError::InvalidDeclaration(format!("invalid name `{text}`")))
607}
608
609pub(super) fn parse_storage(text: &str) -> StorageClass {
610    if text.split_whitespace().any(|word| word == "extern") {
611        StorageClass::Extern
612    } else if text.split_whitespace().any(|word| word == "static") {
613        StorageClass::Static
614    } else if text
615        .split_whitespace()
616        .any(|word| matches!(word, "thread_local" | "_Thread_local" | "__thread"))
617    {
618        StorageClass::ThreadLocal
619    } else {
620        StorageClass::None
621    }
622}
623
624pub(super) fn parse_function_qualifiers(text: &str) -> FunctionQualifiers {
625    FunctionQualifiers {
626        is_const: text.split_whitespace().any(|word| word == "const"),
627        is_volatile: text.split_whitespace().any(|word| word == "volatile"),
628        reference: if text.contains("&&") {
629            Some(crate::ReferenceKind::Rvalue)
630        } else if text.contains('&') {
631            Some(crate::ReferenceKind::Lvalue)
632        } else {
633            None
634        },
635        noexcept: text.contains("noexcept").then_some(true),
636    }
637}
638
639pub(super) fn linkage(language: Language) -> Linkage {
640    match language {
641        Language::C => Linkage::C,
642        Language::Cpp => Linkage::Cpp,
643        Language::ObjectiveC => Linkage::ObjectiveC,
644    }
645}
646
647pub(super) fn record_tag(kind: RecordKind) -> NamedTypeTag {
648    match kind {
649        RecordKind::Struct => NamedTypeTag::Struct,
650        RecordKind::Union => NamedTypeTag::Union,
651        RecordKind::Class => NamedTypeTag::Class,
652        RecordKind::Enum => NamedTypeTag::Enum,
653    }
654}
655
656pub(super) fn starts_with_record(text: &str) -> bool {
657    ["struct ", "union ", "class ", "enum "]
658        .into_iter()
659        .any(|prefix| text.starts_with(prefix))
660}
661
662pub(super) fn contains_record_body(text: &str) -> bool {
663    text.contains('{')
664        && ["struct", "union", "class", "enum"]
665            .iter()
666            .any(|kind| text.contains(kind))
667}
668
669pub(super) fn strip_attributes(mut text: &str) -> &str {
670    for prefix in ["extern ", "static ", "inline ", "__inline ", "__inline__ "] {
671        if let Some(rest) = text.strip_prefix(prefix) {
672            text = rest.trim_start();
673        }
674    }
675    text
676}
677
678pub(super) fn find_top_level(text: &str, needle: char) -> Option<usize> {
679    let mut angles = 0usize;
680    for (index, ch) in text.char_indices() {
681        match ch {
682            '<' => angles += 1,
683            '>' => angles = angles.saturating_sub(1),
684            _ if ch == needle && angles == 0 => return Some(index),
685            _ => {}
686        }
687    }
688    None
689}
690
691pub(super) fn matching_delimiter(
692    text: &str,
693    open: usize,
694    left: char,
695    right: char,
696) -> Option<usize> {
697    let mut depth = 0usize;
698    for (offset, ch) in text[open..].char_indices() {
699        if ch == left {
700            depth += 1;
701        } else if ch == right {
702            depth = depth.checked_sub(1)?;
703            if depth == 0 {
704                return Some(open + offset);
705            }
706        }
707    }
708    None
709}
710
711pub(super) fn split_top_level(text: &str, separator: char) -> Vec<&str> {
712    let mut result = Vec::new();
713    let mut start = 0usize;
714    let mut round = 0usize;
715    let mut angle = 0usize;
716    let mut square = 0usize;
717    let mut brace = 0usize;
718    for (index, ch) in text.char_indices() {
719        match ch {
720            '(' => round += 1,
721            ')' => round = round.saturating_sub(1),
722            '<' => angle += 1,
723            '>' => angle = angle.saturating_sub(1),
724            '[' => square += 1,
725            ']' => square = square.saturating_sub(1),
726            '{' => brace += 1,
727            '}' => brace = brace.saturating_sub(1),
728            _ => {}
729        }
730        if ch == separator && round == 0 && angle == 0 && square == 0 && brace == 0 {
731            result.push(&text[start..index]);
732            start = index + ch.len_utf8();
733        }
734    }
735    result.push(&text[start..]);
736    result
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use crate::Type;
743
744    #[test]
745    fn parses_c_function_and_record() {
746        let unit = TreeSitterHeaderParser
747            .parse(
748                Language::C,
749                "struct Point { int x; int y; };\nextern int distance(struct Point *point);",
750            )
751            .unwrap();
752        assert_eq!(unit.declarations.len(), 2);
753        assert!(matches!(unit.declarations[0], Decl::Record { .. }));
754        assert!(matches!(unit.declarations[1], Decl::Function { .. }));
755    }
756
757    #[test]
758    fn parses_cpp_alias_and_template_type() {
759        let unit = TreeSitterHeaderParser
760            .parse(
761                Language::Cpp,
762                "using Names = std::vector<int>;\nNames names();",
763            )
764            .unwrap();
765        assert_eq!(unit.declarations.len(), 2);
766    }
767
768    #[test]
769    fn rejects_syntax_error() {
770        let error = TreeSitterHeaderParser
771            .parse(Language::C, "int broken(;")
772            .unwrap_err();
773        assert!(matches!(error, ParseError::Syntax(_)));
774    }
775
776    #[test]
777    fn parses_objective_c_interface() {
778        let unit = TreeSitterHeaderParser
779            .parse(
780                Language::ObjectiveC,
781                "@interface Widget : NSObject\n- (int)value;\n@end",
782            )
783            .unwrap();
784        assert!(matches!(
785            unit.declarations.as_slice(),
786            [Decl::ObjectiveCInterface { .. }]
787        ));
788    }
789
790    #[test]
791    fn protocol_qualified_class_type_is_an_object_not_a_template() {
792        // `NSObject<Proto> *` is one protocol-qualified Objective-C object.
793        // Parsing the `*` off first would leave `NSObject<Proto>`, which reads
794        // as a C++ template instantiation and makes the protocol name resolve
795        // against record tags instead of declared protocols.
796        let unit = TreeSitterHeaderParser
797            .parse(
798                Language::ObjectiveC,
799                "@protocol Proto;\n@class NSObject;\n@interface Widget : NSObject\n{\n@protected\n    NSObject<Proto> * _delegate;\n}\n@end",
800            )
801            .unwrap();
802        let [_, _, Decl::ObjectiveCInterface { ivars, .. }] = unit.declarations.as_slice() else {
803            panic!("expected an interface: {:?}", unit.declarations);
804        };
805        let Type::ObjectiveCObject {
806            name, protocols, ..
807        } = &ivars[0].ty
808        else {
809            panic!("expected an Objective-C object type: {:?}", ivars[0].ty);
810        };
811        assert_eq!(
812            name.as_ref().map(ToString::to_string).as_deref(),
813            Some("NSObject")
814        );
815        assert_eq!(
816            protocols
817                .iter()
818                .map(ToString::to_string)
819                .collect::<Vec<_>>(),
820            vec!["Proto".to_owned()]
821        );
822    }
823
824    #[test]
825    fn protocol_qualified_object_types_pass_semantic_validation() {
826        for source in [
827            "@protocol P;\n@class NSObject;\n@interface X : NSObject\n@property (readwrite, weak, atomic) NSObject<P> * d;\n@end\n",
828            "@protocol P;\n@class NSObject;\n@interface X : NSObject\n@property (readwrite, strong, atomic) id<P> d;\n@end\n",
829        ] {
830            let unit = TreeSitterHeaderParser
831                .parse(Language::ObjectiveC, source)
832                .expect("parses");
833            let validation =
834                crate::validate(&unit, crate::ValidationLimits::default()).expect("validates");
835            assert!(
836                validation.semantic_valid,
837                "expected semantic validity for {source:?}: {:?}",
838                validation.diagnostics
839            );
840        }
841    }
842
843    #[test]
844    fn plain_object_pointers_remain_pointers() {
845        let unit = TreeSitterHeaderParser
846            .parse(
847                Language::ObjectiveC,
848                "@class NSString;\n@interface Widget\n{\n@protected\n    NSString * _name;\n}\n@end",
849            )
850            .unwrap();
851        let [_, Decl::ObjectiveCInterface { ivars, .. }] = unit.declarations.as_slice() else {
852            panic!("expected an interface: {:?}", unit.declarations);
853        };
854        assert!(
855            matches!(ivars[0].ty, Type::Pointer { .. }),
856            "unqualified class pointers keep their pointer shape: {:?}",
857            ivars[0].ty
858        );
859    }
860}