xidl-parser 0.65.0

A IDL codegen.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use serde_json::json;
use xidl_parser::hir::{
    Annotation, AnnotationParams, Definition, Pragma, Specification, annotation_id_value,
    const_expr_to_i64,
};
use xidl_parser::parser::{normalize_source_for_tree_sitter, parser_text};
use xidl_parser::typed_ast::{
    AddExpr, AndExpr, AnnotationName, ConstExpr, DecNumber, FloatingPtLiteral, Identifier,
    IntegerLiteral, IntegerSign, Literal, MultExpr, OrExpr, PrimaryExpr, ScopedName, ShiftExpr,
    UnaryExpr, UnaryOperator, XorExpr,
};

fn unique_temp_dir(name: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let path = std::env::temp_dir().join(format!(
        "xidl-parser-coverage-{name}-{}-{nanos}",
        std::process::id()
    ));
    fs::create_dir_all(&path).expect("create temp dir");
    path
}

fn write_file(path: &Path, source: &str) {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).expect("create parent dir");
    }
    fs::write(path, source).expect("write fixture");
}

fn parse_hir_with_path(path: &Path) -> xidl_parser::error::ParserResult<Specification> {
    let source = fs::read_to_string(path).expect("read fixture");
    let typed = xidl_parser::parser::parser_text_with_resolver(
        &source,
        Some(path.to_str().unwrap()),
        &mut xidl_parser::hir::FsIncludeResolver,
    )?;
    Specification::from_typed_ast_with_path(typed, path)
}

fn int_expr(literal: IntegerLiteral) -> ConstExpr {
    ConstExpr(OrExpr::XorExpr(XorExpr::AndExpr(AndExpr::ShiftExpr(
        ShiftExpr::AddExpr(AddExpr::MultExpr(MultExpr::UnaryExpr(
            UnaryExpr::PrimaryExpr(PrimaryExpr::Literal(Literal::IntegerLiteral(literal))),
        ))),
    ))))
}

fn typed_scoped_name(parts: &[&str], is_root: bool) -> ScopedName {
    let mut current = None;
    for part in parts {
        current = Some(ScopedName {
            scoped_name: current.map(Box::new),
            identifier: Identifier((*part).to_string()),
            node_text: String::new(),
        });
    }
    let mut scoped = current.expect("at least one part");
    scoped.node_text = format!("{}{}", if is_root { "::" } else { "" }, parts.join("::"));
    scoped
}

#[test]
fn normalize_annotations_with_brackets_but_keep_strings() {
    let source = r#"@verbatim(language="rust", text=["a", "b"]) const string S = "@keep([1])";"#;
    let normalized = normalize_source_for_tree_sitter(source);

    assert!(normalized.contains("@verbatim"));
    assert!(normalized.contains("\"@keep([1])\""));
    assert!(!normalized.contains("[\"a\", \"b\"]"));
}

#[test]
fn parser_collects_doc_comments_and_complex_template_types() {
    let typed = parser_text(
        r#"
        /// Account balance
        typedef fixed<12, 4> Money;
        typedef map<long, string<8>, 16> Dict;
        typedef Vec<long, string> MyVec;
        "#,
    )
    .expect("parse should succeed");

    let xidl_parser::typed_ast::Specification(defs) = typed;
    let xidl_parser::typed_ast::Definition::TypeDcl(first) = &defs[0] else {
        panic!("expected typedef");
    };
    assert!(matches!(
        first.annotations[0].name,
        AnnotationName::Builtin(ref name) if name == "doc"
    ));

    let xidl_parser::typed_ast::Definition::TypeDcl(second) = &defs[1] else {
        panic!("expected typedef");
    };
    let xidl_parser::typed_ast::TypeDclInner::TypedefDcl(typedef) = &second.decl else {
        panic!("expected typedef decl");
    };
    assert!(matches!(
        typedef.decl.ty,
        xidl_parser::typed_ast::TypeDeclaratorInner::TemplateTypeSpec(
            xidl_parser::typed_ast::TemplateTypeSpec::MapType(_)
        )
    ));

    let xidl_parser::typed_ast::Definition::TypeDcl(third) = &defs[2] else {
        panic!("expected typedef");
    };
    let xidl_parser::typed_ast::TypeDclInner::TypedefDcl(typedef) = &third.decl else {
        panic!("expected typedef decl");
    };
    let xidl_parser::typed_ast::TypeDeclaratorInner::TemplateTypeSpec(
        xidl_parser::typed_ast::TemplateTypeSpec::TemplateType(template),
    ) = &typedef.decl.ty
    else {
        panic!("expected template type");
    };
    assert_eq!(template.ident.0, "Vec");
    assert_eq!(template.args.len(), 2);
}

#[test]
fn hir_parses_pragmas_and_include_errors() {
    let root = unique_temp_dir("pragma");
    let pragma = root.join("pragma.idl");
    let system_include = root.join("system_include.idl");
    let identifier_include = root.join("identifier_include.idl");

    write_file(
        &pragma,
        r#"
        #pragma xidlc package "demo.pkg"
        #pragma xidlc openapi version "1.2.3"
        #pragma xidlc service "https://example.test" "Demo service"
        #pragma vendor keep-me
        "#,
    );
    write_file(&system_include, "#include <dds/core.idl>\n");
    write_file(&identifier_include, "#include SOME_HEADER\n");

    let hir = parse_hir_with_path(&pragma).expect("pragma parse should succeed");
    assert!(matches!(
        hir.0[0],
        Definition::Pragma(Pragma::XidlcPackage(ref value)) if value == "demo.pkg"
    ));
    assert!(matches!(
        hir.0[1],
        Definition::Pragma(Pragma::XidlcOpenApiVersion(ref value)) if value == "1.2.3"
    ));
    assert!(matches!(
        hir.0[2],
        Definition::Pragma(Pragma::XidlcOpenApiService { ref base_url, ref description })
            if base_url == "https://example.test" && description.as_deref() == Some("Demo service")
    ));
    assert!(matches!(
        hir.0[3],
        Definition::Pragma(Pragma::Custom(ref value))
            if value.directive == "#pragma" && value.argument.as_deref() == Some("vendor keep-me")
    ));

    let err = parse_hir_with_path(&system_include).expect_err("system include must fail");
    assert!(err.to_string().contains("unsupported include path syntax"));

    let err = parse_hir_with_path(&identifier_include).expect_err("identifier include must fail");
    assert!(err.to_string().contains("unsupported include identifier"));
}

#[test]
fn expr_and_annotation_helpers_cover_resolution_branches() {
    let annotations = vec![
        Annotation::Appendable,
        Annotation::Builtin {
            name: "extensibility".to_string(),
            params: Some(AnnotationParams::Raw("\"mutable\"".to_string())),
        },
        Annotation::Id {
            value: "42".to_string(),
        },
    ];

    assert_eq!(annotation_id_value(&annotations), Some(42));
    assert_eq!(
        const_expr_to_i64(&int_expr(IntegerLiteral::BinNumber("0b1_010".to_string())).into()),
        Some(10)
    );
    assert_eq!(
        const_expr_to_i64(&int_expr(IntegerLiteral::OctNumber("0O17".to_string())).into()),
        Some(15)
    );
    assert_eq!(
        const_expr_to_i64(&int_expr(IntegerLiteral::HexNumber("0x1f".to_string())).into()),
        Some(31)
    );

    let negative = ConstExpr(OrExpr::XorExpr(XorExpr::AndExpr(AndExpr::ShiftExpr(
        ShiftExpr::AddExpr(AddExpr::MultExpr(MultExpr::UnaryExpr(
            UnaryExpr::UnaryExpr(
                UnaryOperator::Sub,
                PrimaryExpr::Literal(Literal::IntegerLiteral(IntegerLiteral::DecNumber(
                    "7".to_string(),
                ))),
            ),
        ))),
    ))));
    assert_eq!(const_expr_to_i64(&negative.into()), Some(-7));

    let unsupported = ConstExpr(OrExpr::OrExpr(
        Box::new(OrExpr::XorExpr(XorExpr::AndExpr(AndExpr::ShiftExpr(
            ShiftExpr::AddExpr(AddExpr::MultExpr(MultExpr::UnaryExpr(
                UnaryExpr::PrimaryExpr(PrimaryExpr::ScopedName(typed_scoped_name(
                    &["Demo", "VALUE"],
                    true,
                ))),
            ))),
        )))),
        XorExpr::AndExpr(AndExpr::ShiftExpr(ShiftExpr::AddExpr(AddExpr::MultExpr(
            MultExpr::UnaryExpr(UnaryExpr::UnaryExpr(
                UnaryOperator::Not,
                PrimaryExpr::ConstExpr(Box::new(int_expr(IntegerLiteral::DecNumber(
                    "1".to_string(),
                )))),
            )),
        )))),
    ));
    assert_eq!(const_expr_to_i64(&unsupported.into()), None);

    let custom = Specification::from_typed_ast_with_properties(
        parser_text("interface Example { void ping(); };").expect("parse"),
        [("expand_interface".to_string(), json!(false))]
            .into_iter()
            .collect(),
    );
    assert_eq!(custom.0.len(), 1);

    let float: xidl_parser::hir::Literal = Literal::FloatingPtLiteral(FloatingPtLiteral {
        sign: Some(IntegerSign::Minus),
        integer: DecNumber("12".to_string()),
        fraction: DecNumber("5".to_string()),
    })
    .into();
    assert!(matches!(
        float,
        xidl_parser::hir::Literal::FloatingPtLiteral(_)
    ));
}