use std::collections::{BTreeMap, HashSet};
use std::path::Path;
use syn::visit::Visit;
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)
}
type Hit = (String, usize);
fn is_kind_call(expr: &syn::Expr) -> bool {
matches!(expr, syn::Expr::MethodCall(call)
if call.method == "kind" && call.args.is_empty())
}
fn mentions_kind_call(expr: &syn::Expr) -> bool {
#[derive(Default)]
struct Probe(bool);
impl<'ast> Visit<'ast> for Probe {
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
if node.method == "kind" && node.args.is_empty() {
self.0 = true;
}
syn::visit::visit_expr_method_call(self, node);
}
}
let mut probe = Probe::default();
probe.visit_expr(expr);
probe.0
}
fn mentions_kind_ident(expr: &syn::Expr) -> bool {
#[derive(Default)]
struct Probe(bool);
impl<'ast> Visit<'ast> for Probe {
fn visit_ident(&mut self, ident: &'ast proc_macro2::Ident) {
if ident.to_string().contains("kind") {
self.0 = true;
}
}
}
let mut probe = Probe::default();
probe.visit_expr(expr);
probe.0
}
fn as_str_lit(expr: &syn::Expr) -> Option<Hit> {
match expr {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some((s.value(), s.span().start().line)),
_ => None,
},
_ => None,
}
}
fn pattern_literals(pat: &syn::Pat, out: &mut Vec<Hit>) {
struct Collect<'a>(&'a mut Vec<Hit>);
impl<'ast> Visit<'ast> for Collect<'_> {
fn visit_lit_str(&mut self, lit: &'ast syn::LitStr) {
self.0.push((lit.value(), lit.span().start().line));
}
fn visit_pat_guard(&mut self, guard: &'ast syn::PatGuard) {
self.visit_pat(&guard.pat);
}
}
Collect(out).visit_pat(pat);
}
fn parse_matches_body(mac: &syn::Macro) -> Option<(syn::Expr, syn::Pat)> {
mac.parse_body_with(|input: syn::parse::ParseStream| {
let scrutinee: syn::Expr = input.parse()?;
input.parse::<syn::Token![,]>()?;
let pat = syn::Pat::parse_multi_with_leading_vert(input)?;
if input.peek(syn::Token![if]) {
input.parse::<syn::Token![if]>()?;
let _guard: syn::Expr = input.parse()?;
}
if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
Ok((scrutinee, pat))
})
.ok()
}
#[derive(Default)]
struct KindExtractor {
kinds: Vec<Hit>,
fields: Vec<Hit>,
skipped: Vec<usize>,
scopes: Vec<HashSet<String>>,
}
impl KindExtractor {
fn is_kind_local(&self, expr: &syn::Expr) -> bool {
let syn::Expr::Path(path) = expr else {
return false;
};
let Some(ident) = path.path.get_ident() else {
return false;
};
let name = ident.to_string();
self.scopes.iter().any(|scope| scope.contains(&name))
}
fn attributable(&self, expr: &syn::Expr) -> bool {
mentions_kind_call(expr) || self.is_kind_local(expr)
}
fn descend_owned(&mut self, expr: &syn::Expr) {
let mut sub = KindExtractor {
scopes: self.scopes.clone(),
..Default::default()
};
sub.visit_expr(expr);
self.kinds.append(&mut sub.kinds);
self.fields.append(&mut sub.fields);
self.skipped.append(&mut sub.skipped);
}
}
impl<'ast> Visit<'ast> for KindExtractor {
fn visit_block(&mut self, block: &'ast syn::Block) {
self.scopes.push(HashSet::new());
for stmt in &block.stmts {
if let syn::Stmt::Local(local) = stmt {
if let (syn::Pat::Ident(ident), Some(init)) = (&local.pat, &local.init) {
if init.diverge.is_none() && is_kind_call(&init.expr) {
self.scopes
.last_mut()
.expect("scope pushed above")
.insert(ident.ident.to_string());
}
}
}
self.visit_stmt(stmt);
}
self.scopes.pop();
}
fn visit_expr_match(&mut self, node: &'ast syn::ExprMatch) {
if self.attributable(&node.expr) {
for arm in &node.arms {
pattern_literals(&arm.pat, &mut self.kinds);
}
} else if mentions_kind_ident(&node.expr) {
self.skipped.push(node.match_token.span.start().line);
}
syn::visit::visit_expr_match(self, node);
}
fn visit_expr_binary(&mut self, node: &'ast syn::ExprBinary) {
if matches!(node.op, syn::BinOp::Eq(_) | syn::BinOp::Ne(_)) {
if let Some(hit) = as_str_lit(&node.right) {
if self.attributable(&node.left) {
self.kinds.push(hit);
}
}
if let Some(hit) = as_str_lit(&node.left) {
if self.attributable(&node.right) {
self.kinds.push(hit);
}
}
}
syn::visit::visit_expr_binary(self, node);
}
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
if node.method == "child_by_field_name" && node.args.len() == 1 {
if let Some(hit) = as_str_lit(&node.args[0]) {
self.fields.push(hit);
}
}
syn::visit::visit_expr_method_call(self, node);
}
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
if mac.path.is_ident("matches") {
if let Some((scrutinee, pat)) = parse_matches_body(mac) {
if self.attributable(&scrutinee) {
pattern_literals(&pat, &mut self.kinds);
}
self.descend_owned(&scrutinee);
}
}
syn::visit::visit_macro(self, mac);
}
}
struct Extracted {
kinds: Vec<Hit>,
fields: Vec<Hit>,
skipped: usize,
}
fn extract_literals(source: &str) -> Extracted {
let file = syn::parse_file(source).expect("parser source must parse as Rust");
let mut extractor = KindExtractor::default();
extractor.visit_file(&file);
let dedup = |mut hits: Vec<Hit>| -> Vec<Hit> {
hits.sort();
let mut seen: HashSet<String> = HashSet::new();
hits.retain(|(text, _)| seen.insert(text.clone()));
hits
};
Extracted {
kinds: dedup(extractor.kinds),
fields: dedup(extractor.fields),
skipped: extractor.skipped.len(),
}
}
#[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, 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 extracted = extract_literals(&source);
guarded.insert(
name.to_string(),
(
extracted.kinds.len(),
extracted.fields.len(),
extracted.skipped,
),
);
for (kind, line) in &extracted.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}:{line}: node kind {kind:?} unknown to its grammar(s)"
));
}
}
for (field, line) in &extracted.fields {
let known = grammars
.iter()
.any(|g| g.field_id_for_name(field).is_some());
if !known {
offenders.push(format!(
"{name}:{line}: field name {field:?} unknown to its grammar(s)"
));
}
}
}
let total_kinds: usize = guarded.values().map(|(k, _, _)| k).sum();
assert!(
total_kinds >= 350,
"extraction found only {total_kinds} kind literals across {} parsers — \
the syn walker has 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{}\n\
(guarded per file, as (kinds, fields, skipped scrutinees): {guarded:?})",
offenders.join("\n")
);
}
#[test]
fn match_arm_guard_literals_are_not_node_kinds() {
let source = r#"
fn f(sub: Node) {
match sub.kind() {
"identifier" => {}
k if k.contains("type") => {}
"block" if sub.parent().map(|p| p.kind()) == Some("body") => {}
_ => {}
}
}
"#;
let kinds: Vec<String> = extract_literals(source)
.kinds
.into_iter()
.map(|(text, _)| text)
.collect();
assert!(
!kinds.contains(&"type".to_string()),
"guard literal read as a node kind: {kinds:?}"
);
assert!(kinds.contains(&"identifier".to_string()), "{kinds:?}");
assert!(kinds.contains(&"block".to_string()), "{kinds:?}");
}