use std::collections::BTreeMap;
use std::path::Path;
fn grammars_for(file: &str) -> Option<Vec<tree_sitter::Language>> {
let langs: Vec<tree_sitter::Language> = match file {
"rust_lang.rs" => vec![tree_sitter_rust::LANGUAGE.into()],
"python.rs" => vec![tree_sitter_python::LANGUAGE.into()],
"typescript.rs" => vec![
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
tree_sitter_typescript::LANGUAGE_TSX.into(),
tree_sitter_javascript::LANGUAGE.into(),
],
"cpp.rs" => vec![
tree_sitter_cpp::LANGUAGE.into(),
tree_sitter_c::LANGUAGE.into(),
],
"go.rs" => vec![tree_sitter_go::LANGUAGE.into()],
"java.rs" => vec![tree_sitter_java::LANGUAGE.into()],
"csharp.rs" => vec![tree_sitter_c_sharp::LANGUAGE.into()],
"swift.rs" => vec![tree_sitter_swift::LANGUAGE.into()],
"php.rs" => vec![tree_sitter_php::LANGUAGE_PHP.into()],
"html.rs" => vec![tree_sitter_html::LANGUAGE.into()],
"css.rs" => vec![tree_sitter_css::LANGUAGE.into()],
"dart.rs" => vec![tree_sitter_dart::LANGUAGE.into()],
"r.rs" => vec![tree_sitter_r::LANGUAGE.into()],
"julia.rs" => vec![tree_sitter_julia::LANGUAGE.into()],
_ => return None,
};
Some(langs)
}
fn extract_literals(source: &str) -> (Vec<String>, Vec<String>) {
let mut kinds = Vec::new();
let mut fields = Vec::new();
let matches_re =
regex::Regex::new(r#"matches!\s*\(\s*[^,]*\.kind\(\)\s*,\s*([^)]+)\)"#).unwrap();
let lit_re = regex::Regex::new(r#""([A-Za-z_][A-Za-z0-9_]*)""#).unwrap();
for cap in matches_re.captures_iter(source) {
for lit in lit_re.captures_iter(&cap[1]) {
kinds.push(lit[1].to_string());
}
}
let eq_re = regex::Regex::new(r#"\.kind\(\)\s*==\s*"([A-Za-z_][A-Za-z0-9_]*)""#).unwrap();
for cap in eq_re.captures_iter(source) {
kinds.push(cap[1].to_string());
}
let eq_rev_re =
regex::Regex::new(r#""([A-Za-z_][A-Za-z0-9_]*)"\s*==\s*[^;]*\.kind\(\)"#).unwrap();
for cap in eq_rev_re.captures_iter(source) {
kinds.push(cap[1].to_string());
}
let neq_re = regex::Regex::new(r#"\.kind\(\)\s*!=\s*"([A-Za-z_][A-Za-z0-9_]*)""#).unwrap();
for cap in neq_re.captures_iter(source) {
kinds.push(cap[1].to_string());
}
let neq_rev_re =
regex::Regex::new(r#""([A-Za-z_][A-Za-z0-9_]*)"\s*!=\s*[^;]*\.kind\(\)"#).unwrap();
for cap in neq_rev_re.captures_iter(source) {
kinds.push(cap[1].to_string());
}
let field_re =
regex::Regex::new(r#"child_by_field_name\(\s*"([A-Za-z_][A-Za-z0-9_]*)"\s*\)"#).unwrap();
for cap in field_re.captures_iter(source) {
fields.push(cap[1].to_string());
}
kinds.sort();
kinds.dedup();
fields.sort();
fields.dedup();
(kinds, fields)
}
#[test]
fn every_matched_node_kind_exists_in_its_grammar() {
let parsers_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/parsers");
let mut offenders: Vec<String> = Vec::new();
let mut guarded: BTreeMap<String, (usize, usize)> = BTreeMap::new();
for entry in std::fs::read_dir(&parsers_dir).expect("read parsers dir") {
let path = entry.expect("dir entry").path();
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let Some(grammars) = grammars_for(name) else {
continue;
};
let source = std::fs::read_to_string(&path).expect("read parser source");
let (kinds, fields) = extract_literals(&source);
guarded.insert(name.to_string(), (kinds.len(), fields.len()));
for kind in &kinds {
let known = grammars.iter().any(|g| {
g.id_for_node_kind(kind, true) != 0 || g.id_for_node_kind(kind, false) != 0
});
if !known {
offenders.push(format!(
"{name}: node kind {kind:?} unknown to its grammar(s)"
));
}
}
for field in &fields {
let known = grammars
.iter()
.any(|g| g.field_id_for_name(field).is_some());
if !known {
offenders.push(format!(
"{name}: field name {field:?} unknown to its grammar(s)"
));
}
}
}
let total_kinds: usize = guarded.values().map(|(k, _)| k).sum();
assert!(
total_kinds >= 100,
"extraction found only {total_kinds} kind literals across {} parsers — \
the regexes have gone stale against the source style, and the guard \
is no longer guarding. Guarded files: {guarded:?}",
guarded.len()
);
assert!(
offenders.is_empty(),
"node-kind/field literals unknown to their grammars — either a grammar \
bump renamed them (the match arm is now dead and its edge type is \
silently zero) or a literal is misspelled:\n{}",
offenders.join("\n")
);
}