relay-knowledge 1.1.17

Graph-database-based knowledge graph project.
Documentation
use crate::{
    code::SnapshotBuild,
    domain::{FrameworkEdgeKind, FrameworkKind, FrameworkNodeKind},
};

use super::{
    FrameworkFacts, FrameworkFileInput,
    scan::{
        balanced_region, framework_edge, framework_node, quoted_property, relative_module_path,
    },
    template,
};

const ANGULAR_DECORATORS: [(&str, FrameworkNodeKind); 3] = [
    ("Component", FrameworkNodeKind::Component),
    ("Directive", FrameworkNodeKind::Directive),
    ("Pipe", FrameworkNodeKind::Pipe),
];

pub(super) fn looks_like_template(path: &str, content: &str) -> bool {
    path.ends_with(".component.html")
        || content.contains("[(")
        || content.contains("@if")
        || content.contains("@for")
        || content.contains("*ng")
}

pub(super) fn extract(
    build: &SnapshotBuild,
    input: &FrameworkFileInput<'_>,
    facts: &mut FrameworkFacts,
) -> Result<(), crate::code::CodeIndexError> {
    for (decorator, kind) in ANGULAR_DECORATORS {
        let mut search_start = 0usize;
        let marker = format!("@{decorator}");
        while let Some(relative_start) = input
            .content
            .get(search_start..)
            .and_then(|text| text.find(&marker))
        {
            let decorator_start = search_start + relative_start;
            let Some(open) = input
                .content
                .get(decorator_start + marker.len()..)
                .and_then(|text| text.find('('))
                .map(|offset| decorator_start + marker.len() + offset)
            else {
                break;
            };
            let Some(call_end) = balanced_region(input.content, open, b'(', b')') else {
                break;
            };
            let metadata = input
                .content
                .get(open + 1..call_end.saturating_sub(1))
                .unwrap_or_default();
            let class_name = class_name_after(input.content, call_end).unwrap_or(decorator);
            let component_end = class_end_after(input.content, call_end).unwrap_or(call_end);
            let node = framework_node(
                build,
                input,
                FrameworkKind::Angular,
                kind,
                class_name,
                quoted_property(metadata, "selector").map(|(selector, _, _)| selector),
                decorator_start..component_end,
            );
            let component_id = node.node_id.clone();
            facts.nodes.push(node);

            if kind == FrameworkNodeKind::Component {
                extract_component_metadata(build, input, metadata, open + 1, &component_id, facts)?;
                extract_class_bindings(build, input, call_end, component_end, &component_id, facts);
            }
            search_start = call_end;
        }
    }
    Ok(())
}

fn extract_component_metadata(
    build: &SnapshotBuild,
    input: &FrameworkFileInput<'_>,
    metadata: &str,
    metadata_offset: usize,
    component_id: &str,
    facts: &mut FrameworkFacts,
) -> Result<(), crate::code::CodeIndexError> {
    if let Some((template_path, start, end)) = quoted_property(metadata, "templateUrl") {
        let target_path = relative_module_path(input.path, &template_path);
        facts.edges.push(framework_edge(
            build,
            input,
            FrameworkKind::Angular,
            FrameworkEdgeKind::OwnsTemplate,
            component_id,
            (None, Some(target_path)),
            metadata_offset + start..metadata_offset + end,
        ));
    }
    if let Some((inline_template, start, end)) = quoted_property(metadata, "template") {
        let absolute_start = metadata_offset + start + 1;
        let template_node = framework_node(
            build,
            input,
            FrameworkKind::Angular,
            FrameworkNodeKind::Template,
            "inline_template",
            None,
            absolute_start..metadata_offset + end.saturating_sub(1),
        );
        let template_id = template_node.node_id.clone();
        facts.nodes.push(template_node);
        facts.edges.push(framework_edge(
            build,
            input,
            FrameworkKind::Angular,
            FrameworkEdgeKind::OwnsTemplate,
            component_id,
            (Some(template_id.clone()), None),
            metadata_offset + start..metadata_offset + end,
        ));
        template::extract_angular_content(
            build,
            input,
            &inline_template,
            absolute_start,
            &template_id,
            facts,
        )?;
    }
    if let Some(imports) = array_property(metadata, "imports") {
        for (relative, name) in super::scan::identifiers(imports) {
            if !super::scan::expression_identifier(name) {
                continue;
            }
            facts.edges.push(framework_edge(
                build,
                input,
                FrameworkKind::Angular,
                FrameworkEdgeKind::Imports,
                component_id,
                (None, Some(name.to_owned())),
                metadata_offset + relative..metadata_offset + relative + name.len(),
            ));
        }
    }
    Ok(())
}

fn extract_class_bindings(
    build: &SnapshotBuild,
    input: &FrameworkFileInput<'_>,
    class_start: usize,
    class_end: usize,
    component_id: &str,
    facts: &mut FrameworkFacts,
) {
    let class = input
        .content
        .get(class_start..class_end)
        .unwrap_or_default();
    for (marker, kind) in [
        ("@Input", FrameworkNodeKind::Input),
        ("@Output", FrameworkNodeKind::Output),
        ("input(", FrameworkNodeKind::Input),
        ("input.required", FrameworkNodeKind::Input),
        ("output(", FrameworkNodeKind::Output),
    ] {
        for (line_offset, line) in lines_with_offsets(class) {
            if !line.contains(marker) {
                continue;
            }
            let Some(name) = binding_member_name(line, marker) else {
                continue;
            };
            let absolute = class_start + line_offset + line.find(name).unwrap_or_default();
            let node = framework_node(
                build,
                input,
                FrameworkKind::Angular,
                kind,
                name,
                None,
                absolute..absolute + name.len(),
            );
            let node_id = node.node_id.clone();
            facts.nodes.push(node);
            facts.edges.push(framework_edge(
                build,
                input,
                FrameworkKind::Angular,
                FrameworkEdgeKind::Declares,
                component_id,
                (Some(node_id), None),
                absolute..absolute + name.len(),
            ));
        }
    }
}

fn class_name_after(content: &str, offset: usize) -> Option<&str> {
    let tail = content.get(offset..)?;
    let class = tail.find("class ")? + "class ".len();
    tail.get(class..)?
        .split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
        .next()
        .filter(|name| !name.is_empty())
}

fn class_end_after(content: &str, offset: usize) -> Option<usize> {
    let open = content.get(offset..)?.find('{')? + offset;
    balanced_region(content, open, b'{', b'}')
}

fn array_property<'a>(content: &'a str, property: &str) -> Option<&'a str> {
    let start = content.find(property)? + property.len();
    let open = content.get(start..)?.find('[')? + start;
    let end = balanced_region(content, open, b'[', b']')?;
    content.get(open + 1..end.saturating_sub(1))
}

fn lines_with_offsets(content: &str) -> impl Iterator<Item = (usize, &str)> {
    let mut offset = 0usize;
    content.split_inclusive('\n').map(move |line| {
        let current = offset;
        offset += line.len();
        (current, line)
    })
}

fn binding_member_name<'a>(line: &'a str, marker: &str) -> Option<&'a str> {
    let equals = line.find('=')?;
    if marker.starts_with('@') {
        let after = line.find(')')? + 1;
        return line.get(after..equals)?.split_whitespace().last();
    }
    line.get(..equals)?.split_whitespace().last()
}