relay-knowledge 1.1.17

Graph-database-based knowledge graph project.
Documentation
use std::path::Path;

use crate::{
    code::SnapshotBuild,
    domain::{FrameworkEdgeKind, FrameworkKind, FrameworkNodeKind},
};

use super::{
    FrameworkFacts, FrameworkFileInput,
    scan::{framework_edge, framework_node, identifiers},
    template,
};

const MAX_SFC_REGIONS: usize = 16;

pub(super) fn extract(
    build: &SnapshotBuild,
    input: &FrameworkFileInput<'_>,
    facts: &mut FrameworkFacts,
) -> Result<(), crate::code::CodeIndexError> {
    let component_name = vue_component_name(input.path);
    let component = framework_node(
        build,
        input,
        FrameworkKind::Vue,
        FrameworkNodeKind::Component,
        &component_name,
        None,
        0..input.content.len(),
    );
    let component_id = component.node_id.clone();
    facts.nodes.push(component);

    let regions = sfc_regions(input.content);
    if regions.len() > MAX_SFC_REGIONS {
        return Err(crate::code::CodeIndexError::InvalidInput(format!(
            "Vue SFC region budget exceeded: {} > {MAX_SFC_REGIONS}",
            regions.len()
        )));
    }
    for region in regions {
        match region.name {
            "script" => extract_script_macros(
                build,
                input,
                region.content,
                region.content_start,
                &component_id,
                facts,
            ),
            "template" => {
                let node = framework_node(
                    build,
                    input,
                    FrameworkKind::Vue,
                    FrameworkNodeKind::Template,
                    "template",
                    None,
                    region.content_start..region.content_end,
                );
                let template_id = node.node_id.clone();
                facts.nodes.push(node);
                facts.edges.push(framework_edge(
                    build,
                    input,
                    FrameworkKind::Vue,
                    FrameworkEdgeKind::OwnsTemplate,
                    &component_id,
                    (Some(template_id.clone()), None),
                    region.content_start..region.content_end,
                ));
                template::extract_vue_template(
                    build,
                    input,
                    region.content,
                    region.content_start,
                    &template_id,
                    facts,
                );
            }
            _ => {}
        }
    }
    Ok(())
}

pub(super) fn script_mask(content: &str) -> Option<(String, bool)> {
    let regions = sfc_regions(content);
    let mut mask = content
        .bytes()
        .map(|byte| if byte == b'\n' { b'\n' } else { b' ' })
        .collect::<Vec<_>>();
    let mut typescript = false;
    let mut found = false;
    for region in regions.into_iter().filter(|region| region.name == "script") {
        found = true;
        typescript |= region.attributes.contains("lang=\"ts\"")
            || region.attributes.contains("lang='ts'")
            || region.attributes.contains("lang=\"tsx\"")
            || region.attributes.contains("lang='tsx'");
        mask[region.content_start..region.content_end].copy_from_slice(region.content.as_bytes());
    }
    found.then(|| {
        (
            String::from_utf8(mask).expect("Vue script mask remains valid UTF-8"),
            typescript,
        )
    })
}

fn extract_script_macros(
    build: &SnapshotBuild,
    input: &FrameworkFileInput<'_>,
    script: &str,
    base_offset: usize,
    component_id: &str,
    facts: &mut FrameworkFacts,
) {
    for (macro_name, kind) in [
        ("defineProps", FrameworkNodeKind::Prop),
        ("defineEmits", FrameworkNodeKind::Emit),
        ("defineModel", FrameworkNodeKind::Model),
        ("defineSlots", FrameworkNodeKind::Slot),
    ] {
        for (macro_offset, _) in script.match_indices(macro_name) {
            let tail = script
                .get(macro_offset + macro_name.len()..)
                .unwrap_or_default();
            let declarations = macro_declarations(tail, kind);
            for (relative, name) in declarations {
                let absolute = base_offset + macro_offset + macro_name.len() + relative;
                let node = framework_node(
                    build,
                    input,
                    FrameworkKind::Vue,
                    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::Vue,
                    FrameworkEdgeKind::Declares,
                    component_id,
                    (Some(node_id), None),
                    absolute..absolute + name.len(),
                ));
            }
        }
    }
}

fn macro_declarations(tail: &str, kind: FrameworkNodeKind) -> Vec<(usize, String)> {
    if kind == FrameworkNodeKind::Model {
        let name = quoted_argument(tail).unwrap_or("modelValue");
        return vec![(tail.find(name).unwrap_or_default(), name.to_owned())];
    }
    let bounded_end = tail
        .find(")")
        .or_else(|| tail.find("}>"))
        .unwrap_or(tail.len())
        .min(4_096);
    let bounded = tail.get(..bounded_end).unwrap_or_default();
    let mut names = Vec::new();
    for (offset, name) in identifiers(bounded) {
        if matches!(name, "string" | "number" | "boolean" | "true" | "false")
            || names.iter().any(|(_, existing)| existing == name)
        {
            continue;
        }
        names.push((offset, name.to_owned()));
    }
    for quote in ['\'', '"'] {
        for (offset, value) in quoted_values(bounded, quote) {
            if !names.iter().any(|(_, existing)| existing == value) {
                names.push((offset, value.to_owned()));
            }
        }
    }
    names
}

fn quoted_argument(tail: &str) -> Option<&str> {
    let open = tail.find('(')?;
    let rest = tail.get(open + 1..)?.trim_start();
    let quote = rest.chars().next()?;
    if !matches!(quote, '\'' | '"') {
        return None;
    }
    rest.get(1..)?.split(quote).next()
}

fn quoted_values(content: &str, quote: char) -> impl Iterator<Item = (usize, &str)> {
    let mut cursor = 0usize;
    std::iter::from_fn(move || {
        let start = content.get(cursor..)?.find(quote)? + cursor;
        let end = content.get(start + 1..)?.find(quote)? + start + 1;
        cursor = end + quote.len_utf8();
        Some((start + 1, &content[start + 1..end]))
    })
}

struct SfcRegion<'a> {
    name: &'a str,
    attributes: &'a str,
    content: &'a str,
    content_start: usize,
    content_end: usize,
}

fn sfc_regions(content: &str) -> Vec<SfcRegion<'_>> {
    let mut regions = Vec::new();
    let mut cursor = 0usize;
    while regions.len() <= MAX_SFC_REGIONS {
        let Some(open) = content
            .get(cursor..)
            .and_then(|tail| tail.find('<'))
            .map(|value| cursor + value)
        else {
            break;
        };
        let Some(open_end) = content
            .get(open..)
            .and_then(|tail| tail.find('>'))
            .map(|value| open + value)
        else {
            break;
        };
        let header = content.get(open + 1..open_end).unwrap_or_default();
        let name = header.split_whitespace().next().unwrap_or_default();
        if !matches!(name, "script" | "template" | "style") {
            cursor = open_end + 1;
            continue;
        }
        let close_marker = format!("</{name}>");
        let content_start = open_end + 1;
        let Some(relative_end) = content
            .get(content_start..)
            .and_then(|tail| tail.find(&close_marker))
        else {
            break;
        };
        let content_end = content_start + relative_end;
        regions.push(SfcRegion {
            name,
            attributes: header.get(name.len()..).unwrap_or_default(),
            content: content.get(content_start..content_end).unwrap_or_default(),
            content_start,
            content_end,
        });
        cursor = content_end + close_marker.len();
    }
    regions
}

fn vue_component_name(path: &str) -> String {
    Path::new(path)
        .file_stem()
        .and_then(|value| value.to_str())
        .filter(|value| !value.is_empty())
        .unwrap_or("AnonymousComponent")
        .to_owned()
}