arity 0.24.0

A language server, formatter, and linter for R
use super::*;
use rd_ast::{RawRdValue, RdDocument, RdNode, RdTag, producer};

fn section(tag: RdTag, children: Vec<RdNode>) -> RdNode {
    RdNode::tagged(tag, None, children)
}

fn opaque() -> RdNode {
    RdNode::Raw(producer::raw_node(None, None, Vec::new(), None, Vec::new()))
}

fn user_macro(name: &str, definition: &str) -> RdNode {
    let attr = |name: &str, value| {
        producer::raw_attribute(name.into(), producer::raw_object(value, Vec::new()))
    };
    let srcref = producer::raw_attribute(
        "srcref".into(),
        producer::raw_object(
            RawRdValue::Integer(vec![Some(1); 6]),
            vec![
                attr(
                    "srcfile",
                    RawRdValue::Persisted(vec![Some("env::1".into())]),
                ),
                attr("class", RawRdValue::Character(vec![Some("srcref".into())])),
            ],
        ),
    );
    RdNode::Raw(producer::raw_node(
        Some("USERMACRO".into()),
        None,
        vec![RdNode::Text(definition.into())],
        None,
        vec![
            srcref,
            attr("macro", RawRdValue::Character(vec![Some(name.into())])),
        ],
    ))
}

fn cran_pkg() -> Vec<RdNode> {
    vec![
        user_macro(
            r"\CRANpkg",
            r"\href{https://CRAN.R-project.org/package=#1}{\pkg{#1}}stats",
        ),
        section(
            RdTag::Href,
            vec![
                RdNode::group(vec![RdNode::Verb(
                    "https://CRAN.R-project.org/package=stats".into(),
                )]),
                RdNode::group(vec![section(
                    RdTag::Pkg,
                    vec![RdNode::Text("stats".into())],
                )]),
            ],
        ),
    ]
}

#[test]
fn system_macro_preserves_description_and_surrounding_markup() {
    let document = RdDocument::new(vec![section(
        RdTag::Description,
        vec![
            RdNode::Text("Uses ".into()),
            section(RdTag::Emph, cran_pkg()),
            RdNode::Text(" for statistics.".into()),
        ],
    )]);
    let rendered = render_document(&document);
    assert_eq!(
        rendered.sections.description.as_deref(),
        Some("Uses *stats* for statistics.")
    );
    assert!(rendered.issues.is_empty(), "{:?}", rendered.issues);
}

#[test]
fn system_macro_preserves_all_argument_documentation() {
    let document = RdDocument::new(vec![section(
        RdTag::Arguments,
        vec![
            section(
                RdTag::Item,
                vec![
                    RdNode::group(vec![RdNode::Text("x".into())]),
                    RdNode::group(
                        [
                            vec![RdNode::Text("See ".into())],
                            cran_pkg(),
                            vec![RdNode::Text(".".into())],
                        ]
                        .concat(),
                    ),
                ],
            ),
            section(
                RdTag::Item,
                vec![
                    RdNode::group(vec![RdNode::Text("y".into())]),
                    RdNode::group(vec![RdNode::Text("Other values.".into())]),
                ],
            ),
        ],
    )]);
    let rendered = render_document(&document);
    assert_eq!(
        rendered.sections.arguments,
        vec![
            HelpArg {
                name: "x".into(),
                description: "See stats.".into()
            },
            HelpArg {
                name: "y".into(),
                description: "Other values.".into()
            },
        ]
    );
    assert!(rendered.issues.is_empty(), "{:?}", rendered.issues);
}

#[test]
fn system_macro_profiles_render_as_text_without_exposing_expansions() {
    for (nodes, expected) in [
        (cran_pkg(), "stats"),
        (
            vec![
                user_macro(
                    r"\doi",
                    r##"\Sexpr[results=rd]{tools:::Rd_expr_doi("#1")}10.1/x"##,
                ),
                RdNode::tagged(
                    RdTag::Sexpr,
                    Some(vec![RdNode::Text("results=rd".into())]),
                    vec![RdNode::RCode(r#"tools:::Rd_expr_doi("10.1/x")"#.into())],
                ),
            ],
            "10.1/x",
        ),
        (
            vec![
                RdNode::Text("before".into()),
                user_macro(r"\sspace", r"\ifelse{latex}{\out{~}}{ }"),
                section(
                    RdTag::IfElse,
                    vec![
                        RdNode::group(vec![RdNode::Text("latex".into())]),
                        RdNode::group(vec![section(RdTag::Out, vec![RdNode::Verb("~".into())])]),
                        RdNode::group(vec![RdNode::Text(" ".into())]),
                    ],
                ),
                RdNode::Text("after".into()),
            ],
            "before after",
        ),
        (
            vec![section(
                RdTag::I,
                vec![
                    section(RdTag::CranPkg, vec![RdNode::Text("stats".into())]),
                    section(RdTag::Sspace, Vec::new()),
                    section(RdTag::Doi, vec![RdNode::Text("10.1/x".into())]),
                ],
            )],
            "stats 10.1/x",
        ),
    ] {
        let document = RdDocument::new(vec![
            section(RdTag::Title, nodes.clone()),
            section(RdTag::Description, nodes.clone()),
            section(RdTag::Usage, nodes),
        ]);
        let rendered = render_document(&document);
        assert_eq!(rendered.sections.title.as_deref(), Some(expected));
        assert_eq!(rendered.sections.description.as_deref(), Some(expected));
        assert_eq!(rendered.sections.usage.as_deref(), Some(expected));
        assert!(rendered.issues.is_empty(), "{:?}", rendered.issues);
    }
}

#[test]
fn system_macro_body_does_not_hide_opaque_content() {
    let document = RdDocument::new(vec![section(
        RdTag::Description,
        vec![section(RdTag::I, vec![opaque()])],
    )]);
    let rendered = render_document(&document);
    assert!(rendered.sections.description.is_none());
    assert_eq!(rendered.issues.len(), 1);
    assert_eq!(
        rendered.issues[0].path,
        RdAstPath::new(vec![rd_ast::RdAstPathSegment::TopLevel(0)])
            .with_child(0)
            .with_child(0)
    );
}

#[test]
fn unvalidated_system_macro_sequences_remain_opaque() {
    let valid = cran_pkg();
    for invalid in [
        vec![valid[0].clone()],
        vec![valid[0].clone(), RdNode::Text("stats".into())],
        vec![
            user_macro(r"\CRANpkg", "custom definition"),
            valid[1].clone(),
        ],
        vec![user_macro(r"\custom", "stats"), valid[1].clone()],
        vec![valid[0].clone(), opaque()],
    ] {
        let document = RdDocument::new(vec![
            section(RdTag::Title, vec![RdNode::Text("Title".into())]),
            section(RdTag::Description, invalid),
        ]);
        let rendered = render_document(&document);
        assert!(rendered.sections.description.is_none());
        assert_eq!(rendered.sections.title.as_deref(), Some("Title"));
        assert_eq!(rendered.issues.len(), 1);
        assert_eq!(rendered.issues[0].field, "description");
        assert_eq!(
            rendered.issues[0].path,
            RdAstPath::new(vec![rd_ast::RdAstPathSegment::TopLevel(1)]).with_child(0)
        );
    }
}

#[test]
fn canonical_help_matches_all_existing_magrittr_pages() {
    let pkg =
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rindex/magrittr");
    let shared = rd_helpdb::PackageHelpDb::open(&pkg).unwrap();
    let legacy = crate::rindex::lazyload::LazyLoadDb::open(&pkg.join("help/magrittr.rdx")).unwrap();
    let mut count = 0;
    for name in shared.topics() {
        let document = rd_ast::lower_r_object(&shared.raw_topic(name).unwrap()).unwrap();
        let rendered = render_document(&document);
        assert!(rendered.issues.is_empty(), "{name}: {:?}", rendered.issues);
        assert_eq!(
            rendered.sections,
            render_page(&legacy.fetch(name).unwrap()),
            "{name}"
        );
        count += 1;
    }
    assert_eq!(count, 15);
}

#[test]
fn canonical_repeated_sections_and_inline_markup_preserve_presentation() {
    let document = RdDocument::new(vec![
        section(RdTag::Description, vec![RdNode::Text("old".into())]),
        section(
            RdTag::Description,
            vec![
                section(
                    RdTag::Unknown("\\future".into()),
                    vec![RdNode::Text("Keep ".into())],
                ),
                section(RdTag::Code, vec![RdNode::RCode("x  + y".into())]),
                RdNode::Text("\n\nNext ".into()),
                section(RdTag::Emph, vec![RdNode::Text("word".into())]),
            ],
        ),
        section(RdTag::Usage, vec![RdNode::RCode("\nf(x,  y)\n".into())]),
        section(
            RdTag::Arguments,
            vec![section(
                RdTag::Item,
                vec![
                    RdNode::group(vec![RdNode::Text("x, y".into())]),
                    RdNode::group(vec![RdNode::Text("Together.".into())]),
                ],
            )],
        ),
        section(
            RdTag::Arguments,
            vec![section(
                RdTag::Item,
                vec![
                    RdNode::group(vec![RdNode::Text("z".into())]),
                    RdNode::group(vec![RdNode::Text("Last.".into())]),
                ],
            )],
        ),
    ]);
    let rendered = render_document(&document);
    assert!(rendered.issues.is_empty());
    assert_eq!(
        rendered.sections.description.as_deref(),
        Some("Keep `x + y`\n\nNext *word*")
    );
    assert_eq!(rendered.sections.usage.as_deref(), Some("f(x,  y)"));
    assert_eq!(
        rendered
            .sections
            .arguments
            .iter()
            .map(|a| a.name.as_str())
            .collect::<Vec<_>>(),
        ["x, y", "z"]
    );
}

#[test]
fn opaque_content_omits_only_affected_fields() {
    let document = RdDocument::new(vec![
        section(RdTag::Title, vec![RdNode::Text("Title".into())]),
        section(
            RdTag::Description,
            vec![
                RdNode::Text("before".into()),
                opaque(),
                RdNode::Text("after".into()),
            ],
        ),
        section(RdTag::Usage, vec![RdNode::RCode("f()".into())]),
        section(RdTag::Arguments, vec![opaque()]),
    ]);
    let rendered = render_document(&document);
    assert_eq!(rendered.sections.title.as_deref(), Some("Title"));
    assert_eq!(rendered.sections.usage.as_deref(), Some("f()"));
    assert!(rendered.sections.description.is_none());
    assert!(rendered.sections.arguments.is_empty());
    assert_eq!(rendered.issues.len(), 2);
}

#[test]
fn opaque_top_level_section_cannot_silently_keep_an_earlier_value() {
    let document = RdDocument::new(vec![
        section(RdTag::Description, vec![RdNode::Text("old".into())]),
        RdNode::Raw(producer::raw_node(
            Some("\\description".into()),
            None,
            Vec::new(),
            None,
            Vec::new(),
        )),
    ]);
    let rendered = render_document(&document);
    assert!(rendered.sections.description.is_none());
    assert_eq!(rendered.issues.len(), 1);
}

#[test]
fn legacy_renderer_keeps_its_tolerance_of_untyped_text() {
    let page = Robj {
        kind: Rkind::List(vec![Robj {
            kind: Rkind::List(vec![Robj {
                kind: Rkind::Str(vec![Some("kept".into())]),
                attr: vec![],
            }]),
            attr: vec![(
                "Rd_tag".into(),
                Robj {
                    kind: Rkind::Str(vec![Some("\\description".into())]),
                    attr: vec![],
                },
            )],
        }]),
        attr: vec![],
    };
    assert_eq!(render_page(&page).description.as_deref(), Some("kept"));
}