From 9dc68af4f94d1510172433962f3475e341c29ede Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Sat, 11 Feb 2023 20:24:26 +0900 Subject: [PATCH] templater: introduce AST structs This prepares for template aliases support #1190. Unlike revset, template expressions can be of various types, whereas alias substitution will process untyped nodes. That's one reason that ExpressionNode is closer to parsed tree than evaluatable Property structs. Another reason is that it's uneasy to split name/type checking into "parsing" and "building property function" stages. We could do alias expansion at once while building Property functions, but that would make testing harder because Property isn't Debug + PartialEq. --- src/template_parser.rs | 199 ++++++++++++++++++++++++++++++----------- 1 file changed, 146 insertions(+), 53 deletions(-) diff --git a/src/template_parser.rs b/src/template_parser.rs index 638551884..2de5f66ad 100644 --- a/src/template_parser.rs +++ b/src/template_parser.rs @@ -160,6 +160,43 @@ impl error::Error for TemplateParseError { } } +/// AST node without type or name checking. +#[derive(Clone, Debug, PartialEq)] +struct ExpressionNode<'i> { + kind: ExpressionKind<'i>, + span: pest::Span<'i>, +} + +impl<'i> ExpressionNode<'i> { + fn new(kind: ExpressionKind<'i>, span: pest::Span<'i>) -> Self { + ExpressionNode { kind, span } + } +} + +#[derive(Clone, Debug, PartialEq)] +enum ExpressionKind<'i> { + Identifier(&'i str), + Integer(i64), + String(String), + List(Vec>), + FunctionCall(FunctionCallNode<'i>), + MethodCall(MethodCallNode<'i>), +} + +#[derive(Clone, Debug, PartialEq)] +struct FunctionCallNode<'i> { + name: &'i str, + name_span: pest::Span<'i>, + args: Vec>, + args_span: pest::Span<'i>, +} + +#[derive(Clone, Debug, PartialEq)] +struct MethodCallNode<'i> { + object: Box>, + function: FunctionCallNode<'i>, +} + fn parse_string_literal(pair: Pair) -> String { assert_eq!(pair.as_rule(), Rule::literal); let mut result = String::new(); @@ -180,88 +217,87 @@ fn parse_string_literal(pair: Pair) -> String { result } -/* -fn parse_term<'a, C: 'a>( - pair: Pair, - parse_keyword: &impl Fn(Pair) -> TemplateParseResult>, -) -> TemplateParseResult> { +fn parse_function_call_node(pair: Pair) -> TemplateParseResult { + assert_eq!(pair.as_rule(), Rule::function); + let mut inner = pair.into_inner(); + let name = inner.next().unwrap(); + let args_pair = inner.next().unwrap(); + let args_span = args_pair.as_span(); + assert_eq!(name.as_rule(), Rule::identifier); + assert_eq!(args_pair.as_rule(), Rule::function_arguments); + let args = args_pair + .into_inner() + .map(parse_template_node) + .try_collect()?; + Ok(FunctionCallNode { + name: name.as_str(), + name_span: name.as_span(), + args, + args_span, + }) +} + +fn parse_term_node(pair: Pair) -> TemplateParseResult { assert_eq!(pair.as_rule(), Rule::term); let mut inner = pair.into_inner(); let expr = inner.next().unwrap(); + let span = expr.as_span(); let primary = match expr.as_rule() { Rule::literal => { let text = parse_string_literal(expr); - let term = PropertyAndLabels(Property::String(Box::new(Literal(text))), vec![]); - Expression::Property(term) + ExpressionNode::new(ExpressionKind::String(text), span) } Rule::integer_literal => { let value = expr.as_str().parse().map_err(|err| { - TemplateParseError::with_span( - TemplateParseErrorKind::ParseIntError(err), - expr.as_span(), - ) + TemplateParseError::with_span(TemplateParseErrorKind::ParseIntError(err), span) })?; - let term = PropertyAndLabels(Property::Integer(Box::new(Literal(value))), vec![]); - Expression::Property(term) + ExpressionNode::new(ExpressionKind::Integer(value), span) } - Rule::identifier => Expression::Property(parse_keyword(expr)?), + Rule::identifier => ExpressionNode::new(ExpressionKind::Identifier(expr.as_str()), span), Rule::function => { - let mut inner = expr.into_inner(); - let name = inner.next().unwrap(); - let args_pair = inner.next().unwrap(); - assert_eq!(name.as_rule(), Rule::identifier); - assert_eq!(args_pair.as_rule(), Rule::function_arguments); - parse_global_function(name, args_pair, parse_keyword)? + let function = parse_function_call_node(expr)?; + ExpressionNode::new(ExpressionKind::FunctionCall(function), span) } - Rule::template => parse_template_rule(expr, parse_keyword)?, + Rule::template => parse_template_node(expr)?, other => panic!("unexpected term: {other:?}"), }; - match primary { - Expression::Property(property) => { - parse_method_chain(property, inner, parse_keyword).map(Expression::Property) - } - Expression::Template(template) => { - if let Some(chain) = inner.next() { - assert_eq!(chain.as_rule(), Rule::function); - let name = chain.into_inner().next().unwrap(); - Err(TemplateParseError::no_such_method("Template", &name)) - } else { - Ok(Expression::Template(template)) - } - } - } + inner.try_fold(primary, |object, chain| { + assert_eq!(chain.as_rule(), Rule::function); + let span = chain.as_span(); + let method = MethodCallNode { + object: Box::new(object), + function: parse_function_call_node(chain)?, + }; + Ok(ExpressionNode::new( + ExpressionKind::MethodCall(method), + span, + )) + }) } -fn parse_template_rule<'a, C: 'a>( - pair: Pair, - parse_keyword: &impl Fn(Pair) -> TemplateParseResult>, -) -> TemplateParseResult> { +fn parse_template_node(pair: Pair) -> TemplateParseResult { assert_eq!(pair.as_rule(), Rule::template); + let span = pair.as_span(); let inner = pair.into_inner(); - let mut expressions: Vec<_> = inner - .map(|term| parse_term(term, parse_keyword)) - .try_collect()?; - if expressions.len() == 1 { - Ok(expressions.pop().unwrap()) + let mut nodes: Vec<_> = inner.map(parse_term_node).try_collect()?; + if nodes.len() == 1 { + Ok(nodes.pop().unwrap()) } else { - let templates = expressions.into_iter().map(|x| x.into_template()).collect(); - Ok(Expression::Template(Box::new(ListTemplate(templates)))) + Ok(ExpressionNode::new(ExpressionKind::List(nodes), span)) } } -fn parse_template_str<'a, C: 'a>( - template_text: &str, - parse_keyword: impl Fn(Pair) -> TemplateParseResult>, -) -> TemplateParseResult> { +/// Parses text into AST nodes. No type/name checking is made at this stage. +fn parse_template(template_text: &str) -> TemplateParseResult { let mut pairs: Pairs = TemplateParser::parse(Rule::program, template_text)?; let first_pair = pairs.next().unwrap(); if first_pair.as_rule() == Rule::EOI { - Ok(Expression::Template(Box::new(Literal(String::new())))) + let span = first_pair.as_span(); + Ok(ExpressionNode::new(ExpressionKind::List(Vec::new()), span)) } else { - parse_template_rule(first_pair, &parse_keyword) + parse_template_node(first_pair) } } -*/ enum Property<'a, I> { String(Box + 'a>), @@ -874,6 +910,8 @@ pub fn parse_commit_template<'a>( workspace_id: &WorkspaceId, template_text: &str, ) -> TemplateParseResult + 'a>> { + // TODO: use AST node to build expression tree + parse_template(template_text)?; let expression = parse_template_str(template_text, |pair| { parse_commit_keyword(repo, workspace_id, pair) })?; @@ -890,6 +928,61 @@ mod tests { }) } + /// Drops auxiliary data of AST so it can be compared with other node. + fn normalize_tree(node: ExpressionNode) -> ExpressionNode { + fn empty_span() -> pest::Span<'static> { + pest::Span::new("", 0, 0).unwrap() + } + + fn normalize_list(nodes: Vec) -> Vec { + nodes.into_iter().map(normalize_tree).collect() + } + + fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode { + FunctionCallNode { + name: function.name, + name_span: empty_span(), + args: normalize_list(function.args), + args_span: empty_span(), + } + } + + let normalized_kind = match node.kind { + ExpressionKind::Identifier(_) + | ExpressionKind::Integer(_) + | ExpressionKind::String(_) => node.kind, + ExpressionKind::List(nodes) => ExpressionKind::List(normalize_list(nodes)), + ExpressionKind::FunctionCall(function) => { + ExpressionKind::FunctionCall(normalize_function_call(function)) + } + ExpressionKind::MethodCall(method) => { + let object = Box::new(normalize_tree(*method.object)); + let function = normalize_function_call(method.function); + ExpressionKind::MethodCall(MethodCallNode { object, function }) + } + }; + ExpressionNode { + kind: normalized_kind, + span: empty_span(), + } + } + + #[test] + fn test_parse_tree_eq() { + assert_eq!( + normalize_tree(parse_template(r#" commit_id.short(1 ) description"#).unwrap()), + normalize_tree(parse_template(r#"commit_id.short( 1 ) (description)"#).unwrap()), + ); + assert_ne!( + normalize_tree(parse_template(r#" "ab" "#).unwrap()), + normalize_tree(parse_template(r#" "a" "b" "#).unwrap()), + ); + assert_ne!( + normalize_tree(parse_template(r#" "foo" "0" "#).unwrap()), + normalize_tree(parse_template(r#" "foo" 0 "#).unwrap()), + ); + } + #[test] fn test_function_call_syntax() { // Trailing comma isn't allowed for empty argument