blob: e19c1a46bbefcfc8c2984664be37ed5b19819cb2 [file] [log] [blame] [edit]
use std::{fmt, mem};
#[cfg(not(test))]
use proc_macro::{Delimiter, TokenStream, TokenTree};
#[cfg(test)]
use proc_macro2::{Delimiter, TokenStream, TokenTree};
use crate::ast;
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub(crate) struct Error {
msg: String,
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.msg, f)
}
}
pub(crate) fn xflags(ts: TokenStream) -> Result<ast::XFlags> {
let p = &mut Parser::new(ts);
let src = if p.eat_keyword("src") { Some(p.expect_string()?) } else { None };
let doc = opt_doc(p)?;
let mut cmd = cmd(p)?;
cmd.doc = doc;
add_help(&mut cmd);
let res = ast::XFlags { src, cmd };
Ok(res)
}
pub(crate) fn parse_or_exit(ts: TokenStream) -> Result<ast::XFlags> {
let p = &mut Parser::new(ts);
let mut cmd = anon_cmd(p)?;
assert!(cmd.subcommands.is_empty());
add_help(&mut cmd);
let res = ast::XFlags { src: None, cmd };
Ok(res)
}
fn add_help(cmd: &mut ast::Cmd) {
let help = ast::Flag {
arity: ast::Arity::Optional,
name: "help".to_string(),
short: Some("h".to_string()),
doc: Some("Prints help information.".to_string()),
val: None,
};
cmd.flags.push(help);
}
macro_rules! format_err {
($($tt:tt)*) => {
Error { msg: format!($($tt)*) }
// panic!($($tt)*)
};
}
macro_rules! bail {
($($tt:tt)*) => {
return Err(format_err!($($tt)*))
};
}
fn anon_cmd(p: &mut Parser) -> Result<ast::Cmd> {
cmd_impl(p, true)
}
fn cmd(p: &mut Parser) -> Result<ast::Cmd> {
cmd_impl(p, false)
}
fn cmd_impl(p: &mut Parser, anon: bool) -> Result<ast::Cmd> {
let name = if anon {
String::new()
} else {
p.expect_keyword("cmd")?;
cmd_name(p)?
};
let aliases = alias_names(p);
let idx = p.idx;
p.idx += 1;
let mut res = ast::Cmd {
name,
aliases,
doc: None,
args: Vec::new(),
flags: Vec::new(),
subcommands: Vec::new(),
default: false,
idx,
};
if !anon {
p.enter_delim(Delimiter::Brace)?;
}
while !p.end() {
let doc = opt_doc(p)?;
let default = !anon && p.eat_keyword("default");
if !anon && (default || p.at_keyword("cmd")) {
let mut cmd = cmd(p)?;
cmd.doc = doc;
res.subcommands.push(cmd);
if default {
if res.default {
bail!("only one subcommand can be default")
}
res.default = true;
res.subcommands.rotate_right(1);
}
} else {
let arity = arity(p)?;
let is_val = p.lookahead_punct(':', 1);
let name = p.expect_name()?;
if name.starts_with('-') {
let mut flag = flag(p, name)?;
flag.doc = doc;
flag.arity = arity;
res.flags.push(flag)
} else if is_val {
p.expect_punct(':')?;
let ty = ty(p)?;
let val = ast::Val { name, ty };
let arg = ast::Arg { arity, doc, val };
res.args.push(arg);
} else {
bail!("expected `--flag` or `arg: Type`")
}
}
}
if !anon {
p.exit_delim()?;
}
let mut unique_identifiers = std::collections::HashSet::new();
for ident in res.subcommands.iter().map(|cmd| cmd.all_identifiers()).flatten() {
if !unique_identifiers.insert(ident) {
bail!("`{}` is defined multiple times", ident)
}
}
Ok(res)
}
fn flag(p: &mut Parser, name: String) -> Result<ast::Flag> {
let short;
let long;
if name.starts_with("--") {
short = None;
long = name;
} else {
short = Some(name);
if !p.eat_punct(',') {
bail!("long option is required for `{}`", short.unwrap());
}
long = flag_name(p)?;
if !long.starts_with("--") {
bail!("long name must begin with `--`: `{long}`");
}
}
if long == "--help" {
bail!("`--help` flag is generated automatically")
}
let val = opt_val(p)?;
Ok(ast::Flag {
arity: ast::Arity::Required,
name: long[2..].to_string(),
short: short.map(|it| it[1..].to_string()),
doc: None,
val,
})
}
fn opt_val(p: &mut Parser) -> Result<Option<ast::Val>, Error> {
if !p.lookahead_punct(':', 1) {
return Ok(None);
}
let name = p.expect_name()?;
p.expect_punct(':')?;
let ty = ty(p)?;
let res = ast::Val { name, ty };
Ok(Some(res))
}
fn arity(p: &mut Parser) -> Result<ast::Arity> {
if p.eat_keyword("optional") {
return Ok(ast::Arity::Optional);
}
if p.eat_keyword("required") {
return Ok(ast::Arity::Required);
}
if p.eat_keyword("repeated") {
return Ok(ast::Arity::Repeated);
}
if let Some(name) = p.eat_name() {
bail!("expected one of `optional`, `required`, `repeated`, got `{name}`")
}
bail!("expected one of `optional`, `required`, `repeated`, got {:?}", p.ts.pop())
}
fn ty(p: &mut Parser) -> Result<ast::Ty> {
let name = p.expect_name()?;
let res = match name.as_str() {
"PathBuf" => ast::Ty::PathBuf,
"OsString" => ast::Ty::OsString,
_ => ast::Ty::FromStr(name),
};
Ok(res)
}
fn opt_single_doc(p: &mut Parser) -> Result<Option<String>> {
if !p.eat_punct('#') {
return Ok(None);
}
p.enter_delim(Delimiter::Bracket)?;
p.expect_keyword("doc")?;
p.expect_punct('=')?;
let mut res = p.expect_string()?;
if let Some(suf) = res.strip_prefix(' ') {
res = suf.to_string();
}
p.exit_delim()?;
Ok(Some(res))
}
fn opt_doc(p: &mut Parser) -> Result<Option<String>> {
let lines =
core::iter::from_fn(|| opt_single_doc(p).transpose()).collect::<Result<Vec<String>>>()?;
let lines = lines.join("\n");
if lines.is_empty() {
Ok(None)
} else {
Ok(Some(lines))
}
}
fn cmd_name(p: &mut Parser) -> Result<String> {
let name = p.expect_name()?;
if name.starts_with('-') {
bail!("command name can't begin with `-`: `{name}`");
}
Ok(name)
}
fn alias_names(p: &mut Parser) -> Vec<String> {
let mut aliases = vec![];
while let Some(alias) = p.eat_name() {
aliases.push(alias);
}
aliases
}
fn flag_name(p: &mut Parser) -> Result<String> {
let name = p.expect_name()?;
if !name.starts_with('-') {
bail!("flag name should begin with `-`: `{name}`");
}
Ok(name)
}
struct Parser {
stack: Vec<Vec<TokenTree>>,
ts: Vec<TokenTree>,
idx: u8,
}
impl Parser {
fn new(ts: TokenStream) -> Self {
let mut ts = ts.into_iter().collect::<Vec<_>>();
ts.reverse();
Self { stack: Vec::new(), ts, idx: 0 }
}
fn enter_delim(&mut self, delimiter: Delimiter) -> Result<()> {
match self.ts.pop() {
Some(TokenTree::Group(g)) if g.delimiter() == delimiter => {
let mut ts = g.stream().into_iter().collect::<Vec<_>>();
ts.reverse();
let ts = mem::replace(&mut self.ts, ts);
self.stack.push(ts);
}
_ => bail!("expected `{{`"),
}
Ok(())
}
fn exit_delim(&mut self) -> Result<()> {
if !self.end() {
bail!("expected `}}`")
}
self.ts = self.stack.pop().unwrap();
Ok(())
}
fn end(&mut self) -> bool {
self.ts.last().is_none()
}
fn expect_keyword(&mut self, kw: &str) -> Result<()> {
if !self.eat_keyword(kw) {
bail!("expected `{kw}`")
}
Ok(())
}
fn eat_keyword(&mut self, kw: &str) -> bool {
if self.at_keyword(kw) {
self.ts.pop().unwrap();
true
} else {
false
}
}
fn at_keyword(&mut self, kw: &str) -> bool {
match self.ts.last() {
Some(TokenTree::Ident(ident)) => &ident.to_string() == kw,
_ => false,
}
}
fn expect_name(&mut self) -> Result<String> {
self.eat_name().ok_or_else(|| {
let next = self.ts.pop().map(|it| it.to_string()).unwrap_or_default();
format_err!("expected a name, got: `{next}`")
})
}
fn eat_name(&mut self) -> Option<String> {
let mut buf = String::new();
let mut prev_ident = false;
loop {
match self.ts.last() {
Some(TokenTree::Punct(p)) if p.as_char() == '-' => {
prev_ident = false;
buf.push('-');
}
Some(TokenTree::Ident(ident)) if !prev_ident => {
prev_ident = true;
buf.push_str(&ident.to_string());
}
_ => break,
}
self.ts.pop();
}
if buf.is_empty() {
None
} else {
Some(buf)
}
}
fn _expect_ident(&mut self) -> Result<String> {
match self.ts.pop() {
Some(TokenTree::Ident(ident)) => Ok(ident.to_string()),
_ => bail!("expected ident"),
}
}
fn expect_punct(&mut self, punct: char) -> Result<()> {
if !self.eat_punct(punct) {
bail!("expected `{punct}`")
}
Ok(())
}
fn eat_punct(&mut self, punct: char) -> bool {
match self.ts.last() {
Some(TokenTree::Punct(p)) if p.as_char() == punct => {
self.ts.pop();
true
}
_ => false,
}
}
fn lookahead_punct(&mut self, punct: char, n: usize) -> bool {
match self.ts.iter().rev().nth(n) {
Some(TokenTree::Punct(p)) => p.as_char() == punct,
_ => false,
}
}
fn expect_string(&mut self) -> Result<String> {
match self.ts.pop() {
Some(TokenTree::Literal(lit)) if lit.to_string().starts_with('"') => {
let res = str_lit_value(lit.to_string());
Ok(res)
}
_ => bail!("expected a string"),
}
}
}
/// "Parser" a string literal into the corresponding value.
///
/// Really needs support in the proc_macro library:
/// <https://internals.rust-lang.org/t/getting-value-out-of-proc-macro-literal/14140>
fn str_lit_value(lit: String) -> String {
lit.trim_matches('"').replace("\\'", "'")
}