fluidattacks-blends 0.6.0

Blends imperative shell: parsing, AST-graph construction, serialization
Documentation
//! Keep the domain's field-key tables in sync with the tree-sitter grammars
//! they are derived from. This is the single place that reads a grammar's
//! `NODE_TYPES`; it regenerates the committed `fields/<lang>.rs` the domain
//! consumes, so the codegen stays out of the pure, `no_std` domain crate.

use std::collections::BTreeMap;

use proc_macro2::{Ident, Span};
use quote::quote;
use serde_json::Value;
use test_case::test_case;

use crate::language::{Language, LanguageExt};

const KEYWORDS: &[&str] = &[
    "as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn",
    "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
    "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use",
    "where", "while", "async", "await", "abstract", "become", "box", "do", "final", "macro",
    "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
];

fn ident(name: &str) -> Ident {
    if KEYWORDS.contains(&name) {
        Ident::new_raw(name, Span::call_site())
    } else {
        Ident::new(name, Span::call_site())
    }
}

fn field_tables(node_types_json: &str) -> BTreeMap<String, BTreeMap<String, bool>> {
    let parsed: Value = serde_json::from_str(node_types_json).expect("parse grammar NODE_TYPES");
    let mut tables = BTreeMap::new();
    for entry in parsed.as_array().into_iter().flatten() {
        let Some(node_type) = entry.get("type").and_then(Value::as_str) else {
            continue;
        };
        let Some(fields) = entry.get("fields").and_then(Value::as_object) else {
            continue;
        };
        if fields.is_empty() {
            continue;
        }
        let mut table = BTreeMap::new();
        for (name, meta) in fields {
            let required = meta
                .get("required")
                .and_then(Value::as_bool)
                .unwrap_or(false);
            table.insert(name.clone(), required);
        }
        tables.insert(node_type.to_owned(), table);
    }
    tables
}

fn rustfmt(code: &str) -> String {
    use std::io::Write;
    use std::process::{Command, Stdio};

    let mut child = Command::new("rustfmt")
        .args(["--edition", "2021"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .expect("spawn rustfmt");
    child
        .stdin
        .take()
        .expect("rustfmt stdin")
        .write_all(code.as_bytes())
        .expect("write source to rustfmt");
    let output = child.wait_with_output().expect("collect rustfmt output");
    String::from_utf8(output.stdout).expect("rustfmt produced valid utf-8")
}

fn generate(lang: &str, node_types_json: &str) -> String {
    let modules = field_tables(node_types_json)
        .into_iter()
        .map(|(node_type, table)| {
            let module = ident(&node_type);
            let consts = table.into_iter().map(|(name, required)| {
                let konst = ident(&name.to_uppercase());
                let key = format!("{name}_id");
                quote! {
                    pub const #konst: Field<#required> = Field::new(#key);
                }
            });
            quote! {
                pub mod #module {
                    use crate::syntax::fields::Field;
                    #(#consts)*
                }
            }
        });
    let body =
        rustfmt(&quote! { #(#modules)* }.to_string()).replace("}\npub mod ", "}\n\npub mod ");
    format!("//! Generated from the tree-sitter `{lang}` grammar node-types. Do not edit by hand.\n\n{body}")
}

fn assert_in_sync(lang: &str, node_types_json: &str) {
    use std::fs;
    use std::path::Path;

    let generated = generate(lang, node_types_json);
    let path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join(format!("../domain/src/syntax/fields/{lang}.rs"));
    let committed = fs::read_to_string(&path).unwrap_or_default();
    if committed != generated {
        fs::write(&path, &generated).expect("write regenerated field table");
        panic!(
            "regenerated crates/domain/src/syntax/fields/{lang}.rs from the {lang} grammar; \
             review the changes and add them to your commit if correct"
        );
    }
}

#[test_case(Language::CSharp, "c_sharp")]
#[test_case(Language::Elixir, "elixir")]
#[test_case(Language::Go, "go")]
#[test_case(Language::Hcl, "hcl")]
#[test_case(Language::Java, "java")]
#[test_case(Language::JavaScript, "javascript")]
#[test_case(Language::Json, "json")]
#[test_case(Language::Kotlin, "kotlin")]
#[test_case(Language::Php, "php")]
#[test_case(Language::Ruby, "ruby")]
#[test_case(Language::Rust, "rust")]
#[test_case(Language::Scala, "scala")]
#[test_case(Language::Swift, "swift")]
#[test_case(Language::TypeScript, "typescript")]
#[test_case(Language::Python, "python")]
#[test_case(Language::Yaml, "yaml")]
fn vendored_field_tables_match_grammar(language: Language, stem: &str) {
    let node_types = language
        .node_types()
        .expect("a vendored language must expose grammar node-types");
    assert_in_sync(stem, node_types);
}