use ast_grep_core::tree_sitter::StrDoc;
use ast_grep_core::{AstGrep, Pattern};
use ast_grep_language::SupportLang;
use thiserror::Error;
use crate::ast_lang;
use crate::ast_render::{self, Doc};
#[derive(Debug, Error)]
pub enum AstFailure {
#[error(
"cannot tell what language {origin} is, so `ast:` has nothing to parse it with; name it with `lang:` (this build parses: {available})"
)]
LanguageUnknown {
origin: String,
available: String,
},
#[error(
"this mmz was built without the `{name}` grammar; rebuild it with `--features lang-{name}` (this build parses: {available})"
)]
LanguageMissing {
name: String,
available: String,
},
#[error("`{name}` is not a language mmz can parse; this build parses: {available}")]
LanguageUnsupported {
name: String,
available: String,
},
#[error("{origin} is not valid UTF-8, so it cannot be parsed as {lang} source")]
NotText {
origin: String,
lang: String,
},
#[error("`{pattern}` is not a usable {lang} pattern: {reason}")]
Pattern {
pattern: String,
lang: String,
reason: String,
},
#[error("{origin} could not be parsed as {lang}: {reason}")]
Unparsable {
origin: String,
lang: String,
reason: String,
},
#[error(
"`{pattern}` matched nothing in {origin}; a pattern that matches nothing reports the same digest whatever the source says, leaving every rule that names it fresh forever — fix the pattern, or set `allow_empty: true` if no match really is a valid input"
)]
Empty {
pattern: String,
origin: String,
},
#[error(
"`capture:` names `{name}`, which `{pattern}` does not define (it defines {defined}); an anonymous `$$$` or a `$_` binds nothing and cannot be named — fix the name, or give the pattern a `${name}` to capture"
)]
CaptureUndefined {
name: String,
pattern: String,
defined: String,
},
}
pub(crate) fn resolve_lang(
declared: Option<&str>,
path: Option<&std::path::Path>,
origin: &str,
) -> Result<SupportLang, AstFailure> {
let Some(name) = declared else {
return path
.and_then(ast_lang::by_extension)
.ok_or_else(|| AstFailure::LanguageUnknown {
origin: origin.to_owned(),
available: ast_lang::available(),
});
};
if let Some(grammar) = ast_lang::by_name(name) {
return Ok(grammar);
}
let available = ast_lang::available();
if ast_lang::is_known(name) {
return Err(AstFailure::LanguageMissing {
name: name.to_owned(),
available,
});
}
Err(AstFailure::LanguageUnsupported {
name: name.to_owned(),
available,
})
}
#[must_use]
pub fn language_summary() -> String {
let count = ast_lang::count();
let noun = if count == 1 { "lang" } else { "langs" };
format!("{count} ast {noun}")
}
pub(crate) fn select<'n>(
lang: SupportLang,
pattern: &str,
captures: Option<&'n [String]>,
input: &[u8],
origin: &str,
) -> Result<Vec<Vec<u8>>, AstFailure> {
let source = std::str::from_utf8(input).map_err(|_| AstFailure::NotText {
origin: origin.to_owned(),
lang: format!("{lang:?}"),
})?;
let compiled = compile(lang, pattern)?;
let names: Option<Vec<&'n str>> = captures
.map(|listed| ordered_captures(&compiled, listed, pattern))
.transpose()?;
let doc = StrDoc::try_new(source, lang).map_err(|reason| AstFailure::Unparsable {
origin: origin.to_owned(),
lang: format!("{lang:?}"),
reason,
})?;
let root: AstGrep<Doc> = AstGrep::doc(doc);
Ok(root
.root()
.find_all(&compiled)
.map(|found| match &names {
Some(names) => ast_render::render_captures(found.get_env(), names),
None => ast_render::render(&found),
})
.collect())
}
fn compile(lang: SupportLang, pattern: &str) -> Result<Pattern, AstFailure> {
let compiled = Pattern::try_new(pattern, lang).map_err(|err| AstFailure::Pattern {
pattern: pattern.to_owned(),
lang: format!("{lang:?}"),
reason: err.to_string(),
})?;
if compiled.has_error() {
return Err(AstFailure::Pattern {
pattern: pattern.to_owned(),
lang: format!("{lang:?}"),
reason: "the grammar could not parse it and recovered into an error node, so it \
would match nothing at all"
.to_owned(),
});
}
Ok(compiled)
}
fn ordered_captures<'n>(
compiled: &Pattern,
listed: &'n [String],
pattern: &str,
) -> Result<Vec<&'n str>, AstFailure> {
let defined = compiled.defined_vars();
if let Some(unknown) = listed.iter().find(|name| !defined.contains(name.as_str())) {
let mut known: Vec<&str> = defined.into_iter().collect();
known.sort_unstable();
return Err(AstFailure::CaptureUndefined {
name: unknown.clone(),
pattern: pattern.to_owned(),
defined: describe(&known),
});
}
let mut names: Vec<&str> = listed.iter().map(String::as_str).collect();
names.sort_unstable();
names.dedup();
Ok(names)
}
fn describe(known: &[&str]) -> String {
if known.is_empty() {
return "no metavariables at all".to_owned();
}
known
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<String>>()
.join(", ")
}
#[cfg(test)]
#[path = "ast_tests.rs"]
mod tests;