alef 0.114.0

Opinionated polyglot binding generator for Rust libraries
Documentation
use anyhow::{Context as _, Result, ensure};
use serde::{Deserialize, Serialize};

mod lexical;
mod metadata;
#[cfg(test)]
mod tests;

pub(crate) use metadata::metadata_comments;
#[cfg(test)]
use metadata::records;

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Record {
    owner: String,
    factory: String,
    fields: usize,
    chunks: Vec<Chunk>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
enum Chunk {
    Text { text: String },
    Field { field: usize },
}

pub(crate) fn rewrite(source: &str, bridge: &str, mirror: &str) -> Result<String> {
    let records = read_records(mirror)?;
    if records.is_empty() {
        return Ok(source.to_string());
    }
    let bridge_mask = lexical::mask(bridge)?;
    let source_mask = lexical::mask(source)?;
    let mut edits = Vec::new();
    let mut seen = std::collections::BTreeSet::new();
    for record in records {
        ensure!(
            seen.insert((record.owner.clone(), record.factory.clone())),
            "duplicate Dart error display metadata"
        );
        edits.push(
            edit(source, &source_mask, &bridge_mask, &record)
                .with_context(|| format!("restoring Dart error display for {}.{}", record.owner, record.factory))?,
        );
    }
    edits.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
    ensure!(
        edits.windows(2).all(|pair| pair[0].0.start >= pair[1].0.end),
        "overlapping Freezed error implementations"
    );
    let mut result = source.to_string();
    for (range, replacement) in edits {
        result.replace_range(range, &replacement);
    }
    Ok(result)
}

fn read_records(mirror: &str) -> Result<Vec<Record>> {
    mirror
        .lines()
        .filter_map(|line| line.trim_start().strip_prefix("// ~keep alef-error-display: "))
        .map(|json| serde_json::from_str(json).context("reading generated Dart error display metadata"))
        .collect()
}

fn one_match<'a>(source: &'a str, pattern: &str, description: &str) -> Result<regex::Match<'a>> {
    let regex = regex::Regex::new(pattern)?;
    let matches: Vec<_> = regex.find_iter(source).collect();
    ensure!(
        matches.len() == 1,
        "expected one {description}, found {}",
        matches.len()
    );
    Ok(matches[0])
}

fn factory_class(bridge: &str, record: &Record) -> Result<(String, Vec<String>)> {
    let pattern = format!(
        r"\bfactory\s+{}\.{}\s*\(",
        regex::escape(&record.owner),
        regex::escape(&record.factory)
    );
    let declaration = one_match(bridge, &pattern, "FRB error factory")?;
    let end = lexical::closing(bridge, declaration.end() - 1)?;
    let fields = lexical::parameters(&bridge[declaration.end()..end])?;
    ensure!(fields.len() == record.fields, "FRB factory field cardinality changed");
    let suffix = regex::Regex::new(r"^\s*=\s*([$_\p{XID_Start}][$_\p{XID_Continue}]*)\s*;")?;
    suffix
        .captures(&bridge[end + 1..])
        .and_then(|captures| captures.get(1))
        .map(|name| (name.as_str().to_string(), fields))
        .context("reading actual Freezed error class alias")
}

fn class_body<'a>(source: &'a str, name: &str) -> Result<(&'a str, usize)> {
    let pattern = format!(r"\bclass\s+{}\b[^{{}}]*\{{", regex::escape(name));
    let declaration = one_match(source, &pattern, "Freezed error class")?;
    let end = lexical::closing(source, declaration.end() - 1)?;
    Ok((&source[declaration.end()..end], declaration.end()))
}

fn field_names(body: &str, class: &str, count: usize) -> Result<Vec<String>> {
    let pattern = format!(r"\b{}\s*\(", regex::escape(class));
    let constructor = one_match(body, &pattern, "Freezed error constructor")?;
    let end = lexical::closing(body, constructor.end() - 1)?;
    let parameters = lexical::parameters(&body[constructor.end()..end])?;
    ensure!(
        parameters.len() == count,
        "expected {count} mirrored fields, found {}",
        parameters.len()
    );
    Ok(parameters)
}

fn expression(record: &Record, fields: &[String]) -> Result<String> {
    record
        .chunks
        .iter()
        .map(|chunk| {
            Ok(match chunk {
                Chunk::Text { text } => serde_json::to_string(text)?.replace('$', "\\$"),
                Chunk::Field { field } => format!(
                    "{}.toString()",
                    fields.get(*field).context("missing mirrored display field")?
                ),
            })
        })
        .collect::<Result<Vec<_>>>()
        .map(|pieces| pieces.join(" + "))
}

fn edit(source: &str, mask: &str, bridge: &str, record: &Record) -> Result<(std::ops::Range<usize>, String)> {
    let (class, fields) = factory_class(bridge, record)?;
    let (body, offset) = class_body(mask, &class)?;
    let properties = field_names(body, &class, record.fields)?;
    ensure!(
        fields
            .iter()
            .all(|field| properties.iter().filter(|name| *name == field).count() == 1),
        "FRB factory fields do not uniquely bind Freezed constructor properties"
    );
    let method = one_match(
        body,
        r"\bString\s+toString\s*\(\s*\)\s*\{",
        "Freezed error toString method",
    )?;
    let start = offset + method.start();
    let end = lexical::closing(mask, offset + method.end() - 1)? + 1;
    let line_start = source[..start].rfind('\n').map_or(0, |index| index + 1);
    let indent = &source[line_start..start];
    ensure!(
        indent.trim().is_empty(),
        "Freezed toString declaration must begin a line"
    );
    let replacement = super::template_env::render(
        "dart_error_display_method.jinja",
        crate::alef_context! {
            indent => indent, expression => expression(record, &fields)?,
        },
    );
    Ok((line_start..end, replacement.trim_end_matches('\n').to_string()))
}