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("e! { #(#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);
}