devscout-rs 0.10.0

Fast code index for C# and TypeScript codebases -- map, find, refs, impact from the CLI.
use super::*;
use crate::ngtemplate::GapReason;

fn components(src: &str) -> Vec<TsComponent> {
    let units = crate::parse::utf16_units(src);
    let grammar = crate::parse::ts_grammar_for("a.ts").expect("ts grammar");
    let tree = crate::parse::parse_ts_js(&units, grammar).expect("fixture parses");
    extract_ts_fragment(tree.root_node(), &crate::parse::utf16_bytes(&units)).components
}

fn member_rows(c: &TsComponent) -> Vec<(&str, &str, usize)> {
    c.members
        .iter()
        .map(|m| (m.name.as_str(), m.kind.as_str(), m.line))
        .collect()
}

#[test]
fn angular_templates_ac_1_members_keep_kinds_and_first_lines() {
    let src = "@Component({ templateUrl: './a.html' })\n\
               export class Panel {\n\
               static registry = 1;\n\
               title = '';\n\
               #secret = 2;\n\
               'quoted' = 3;\n\
               [computed] = 4;\n\
               get open() { return true; }\n\
               set open(v) {}\n\
               save(): void;\n\
               save(x?: number) {}\n\
               static make() {}\n\
               constructor(private svc: object, readonly id: string, plain: number) {}\n\
               }";
    let found = components(src);
    assert_eq!(found.len(), 1);
    assert_eq!(found[0].name, "Panel");
    assert_eq!(
        member_rows(&found[0]),
        [
            ("title", "field", 4),
            ("#secret", "field", 5),
            ("quoted", "field", 6),
            ("open", "property", 8),
            ("save", "method", 10),
            ("svc", "field", 13),
            ("id", "field", 13),
        ]
    );
    assert_eq!(
        found[0].template_url,
        Some(TsTemplateUrl {
            spec: "./a.html".into(),
            line: 1
        })
    );
}

#[test]
fn angular_templates_ac_1_only_a_component_decorator_with_an_object_counts() {
    let src = "@Injectable({}) export class Service { a = 1; }\n\
               @Component('x') export class NotObject { b = 1; }\n\
               @ns.Component({}) export class Dotted { c = 1; }\n\
               export class Plain { d = 1; }\n\
               @Component({}) class Local { e = 1; }\n\
               export @Component({}) class Late { f = 1; }";
    let names: Vec<String> = components(src).into_iter().map(|c| c.name).collect();
    assert_eq!(names, ["Local", "Late"]);
}

#[test]
fn angular_templates_ac_3_an_inline_template_counts_lines_from_its_string() {
    let src = "@Component({\n  template: `\n<p>{{ first }}</p>\n<b\\n>{{ second }}</b>`,\n})\nexport class Inline {}";
    let found = components(src);
    let scan = found[0].inline.as_ref().expect("inline scan");
    let reads: Vec<(&str, usize)> = scan
        .reads
        .iter()
        .map(|r| (r.name.as_str(), r.line))
        .collect();
    assert_eq!(reads, [("first", 3), ("second", 4)]);
}

#[test]
fn angular_templates_ac_3_escapes_are_undone_without_moving_lines() {
    let src = "@Component({ template: '<p title=\\'{{ a }}\\'>\\\n{{ b }}</p>' })\nexport class Escaped {}";
    let found = components(src);
    let scan = found[0].inline.as_ref().expect("inline scan");
    let reads: Vec<(&str, usize)> = scan
        .reads
        .iter()
        .map(|r| (r.name.as_str(), r.line))
        .collect();
    assert_eq!(reads, [("a", 1), ("b", 2)]);
}

#[test]
fn angular_templates_ac_7_a_value_that_is_not_one_literal_is_a_gap() {
    let src = "const PATH = './a.html';\n\
               @Component({\n\
               templateUrl: PATH,\n\
               template: `<p>${PATH}</p>`,\n\
               })\n\
               export class Gaps {}\n\
               @Component({ templateUrl: './a' + '.html', template })\n\
               export class More {}";
    let found = components(src);
    let gaps = |c: &TsComponent| -> Vec<(usize, GapReason)> {
        c.gaps.iter().map(|g| (g.line, g.reason)).collect()
    };
    assert_eq!(
        gaps(&found[0]),
        [
            (3, GapReason::TemplateUrlNonLiteral),
            (4, GapReason::InlineTemplateSubstitution)
        ]
    );
    assert_eq!(
        gaps(&found[1]),
        [
            (7, GapReason::TemplateUrlNonLiteral),
            (7, GapReason::InlineTemplateSubstitution)
        ]
    );
    assert!(found[0].template_url.is_none() && found[0].inline.is_none());
}

#[test]
fn angular_templates_ac_1_a_file_without_a_component_serializes_as_before() {
    let units = crate::parse::utf16_units("export class Plain { a = 1; }");
    let grammar = crate::parse::ts_grammar_for("a.ts").expect("ts grammar");
    let tree = crate::parse::parse_ts_js(&units, grammar).expect("fixture parses");
    let fragment = extract_ts_fragment(tree.root_node(), &crate::parse::utf16_bytes(&units));
    let json = serde_json::to_string(&fragment).unwrap();
    assert!(!json.contains("components"), "{json}");
}