webots-proto 0.1.3

Facade crate for parsing, templating, resolving, and validating Webots PROTO.
Documentation
use std::collections::HashSet;
use std::path::{Path, PathBuf};

use webots_proto_ast::Proto;
use webots_proto_schema::{Diagnostic, DiagnosticSet, Severity};

use crate::Error;

pub(crate) fn validate_local_externproto_naming(
    root_proto_path: &Path,
    root_document: &Proto,
) -> Result<DiagnosticSet, Error> {
    let mut visited = HashSet::new();
    let mut diagnostics = DiagnosticSet::new();
    let canonical_root = root_proto_path.canonicalize()?;
    collect_mismatches_recursive(
        &canonical_root,
        root_document,
        &mut visited,
        &mut diagnostics,
    )?;
    Ok(diagnostics)
}

fn collect_mismatches_recursive(
    proto_path: &Path,
    document: &Proto,
    visited: &mut HashSet<PathBuf>,
    diagnostics: &mut DiagnosticSet,
) -> Result<(), Error> {
    let canonical_proto = proto_path.canonicalize()?;
    if !visited.insert(canonical_proto.clone()) {
        return Ok(());
    }

    let base_dir = canonical_proto.parent().ok_or_else(|| {
        std::io::Error::other(format!(
            "PROTO has no parent: {}",
            canonical_proto.display()
        ))
    })?;

    for extern_proto in &document.externprotos {
        let import_url = extern_proto.url.trim();
        if import_url.starts_with("webots://")
            || import_url.starts_with("http://")
            || import_url.starts_with("https://")
        {
            continue;
        }

        let import_path = if Path::new(import_url).is_absolute() {
            PathBuf::from(import_url)
        } else {
            base_dir.join(import_url)
        };
        let import_file_name = import_path
            .file_name()
            .and_then(|name| name.to_str())
            .ok_or_else(|| std::io::Error::other("Imported PROTO path is not valid UTF-8"))?
            .to_string();
        let canonical_import = import_path.canonicalize()?;
        let imported_content = std::fs::read_to_string(&canonical_import)?;
        let imported_document: Proto = imported_content.parse()?;
        let imported_proto = imported_document.proto.clone().ok_or_else(|| {
            std::io::Error::other(format!(
                "Imported file {} does not contain a PROTO definition",
                canonical_import.display()
            ))
        })?;
        let expected_file_name = format!("{}.proto", imported_proto.name);
        if import_file_name != expected_file_name {
            diagnostics.add(Diagnostic {
                span: extern_proto.span.clone(),
                severity: Severity::Error,
                message: format!(
                    "EXTERNPROTO '{}' resolves to '{}' declaring PROTO '{}', expected file name '{}'",
                    import_url,
                    canonical_import.display(),
                    imported_proto.name,
                    expected_file_name
                ),
                suggestion: Some(format!("Reference '{}'", expected_file_name)),
            });
        }

        collect_mismatches_recursive(&canonical_import, &imported_document, visited, diagnostics)?;
    }

    Ok(())
}