feat(parser): AST Separated List
Implements an AST view for lists that contain nodes separated by tokens, for example, an array expression where the elements are separated by commas.
MODIFY
crates/rslint_parser/src/ast.rs
crates/rslint_parser/src/ast.rs
@@ -185,8 +185,181 @@ impl<N: AstNode> IntoIterator for AstNodeList<N> {
}
}
+#[derive(Debug, Clone)]
+pub struct AstSeparatedElement<N> {
+ node: N,
+ trailing_separator: Option<SyntaxToken>,
+}
+
+/// List of nodes where every two nodes are separated by a token.
+/// For example, the elements of an array where every two elements are separated by a comma token.
+#[derive(Debug, Clone)]
+pub struct AstSeparatedList<N> {
+ list: SyntaxList,
+ ph: PhantomData<N>,
+}
+
+impl<N: AstNode> AstSeparatedList<N> {
+ fn new(list: SyntaxList) -> Self {
+ Self {
+ list,
+ ph: PhantomData,
+ }
+ }
+
+ /// Returns an iterator over all nodes with their trailing separator
+ pub fn elements(&self) -> AstSeparatedListElementsIterator<N> {
+ AstSeparatedListElementsIterator {
+ next: self.list.iter(),
+ ph: PhantomData,
+ }
+ }
+
+ /// Returns an iterator over all separator tokens
+ pub fn separators(&self) -> impl Iterator<Item = SyntaxToken> {
+ self.elements()
+ .flat_map(|element| element.trailing_separator)
+ }
+
+ /// Returns an iterator over all nodes
+ pub fn iter(&self) -> AstSeparatedListNodesIterator<N> {
+ AstSeparatedListNodesIterator {
+ inner: self.elements(),
+ }
+ }
+
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ pub fn len(&self) -> usize {
+ // TODO 1724 replace with (self.list.len() + 1) << 2 once trivia are attached to tokens
+ self.iter().count()
+ }
+
+ pub fn trailing_separator(&self) -> Option<SyntaxToken> {
+ // TODO 1724: Replace with simple match once trivia is no longer stored in the body
+ // match self.list.last() {
+ // NodeOrToken::Token(token) => Some(token),
+ // _ => None
+ // }
+
+ self.elements().last()?.trailing_separator
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct AstSeparatedListElementsIterator<N> {
+ next: SyntaxElementChildren,
+ ph: PhantomData<N>,
+}
+
+impl<N> AstSeparatedListElementsIterator<N> {
+ // TODO 1724: Replace with call to next once trivia are no longer stored in tokens and errors are part of the union types.
+ fn next_non_trivia_or_error(&mut self) -> Option<SyntaxElement> {
+ self.next.find_map(|element| match &element {
+ NodeOrToken::Node(node) => {
+ if node.kind() == SyntaxKind::ERROR {
+ None
+ } else {
+ Some(element)
+ }
+ }
+ NodeOrToken::Token(token) => {
+ if token.kind().is_trivia() {
+ None
+ } else {
+ Some(element)
+ }
+ }
+ })
+ }
+}
+
+impl<N: AstNode> Iterator for AstSeparatedListElementsIterator<N> {
+ type Item = AstSeparatedElement<N>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let node_or_token = self.next_non_trivia_or_error()?;
+
+ let element = match node_or_token {
+ // The node for this element is missing if the next child is a token instead of a node.
+ NodeOrToken::Token(token) => panic!(
+ "Missing element in separated list, found {:?} token instead",
+ token
+ ),
+ NodeOrToken::Node(node) => {
+ let separator = self.next.find_map(|element| {
+ match element {
+ NodeOrToken::Node(_) => panic!("Expected separator but found node. Two nodes must always be separated by a separator token."),
+ NodeOrToken::Token(token) => if token.kind().is_trivia() {
+ None
+ } else {
+ Some(token)
+ }
+ }
+ });
+
+ AstSeparatedElement {
+ node: node.to::<N>(),
+ trailing_separator: separator,
+ }
+ }
+ };
+
+ Some(element)
+ }
+}
+
+impl<N: AstNode> FusedIterator for AstSeparatedListElementsIterator<N> {}
+
+#[derive(Debug, Clone)]
+pub struct AstSeparatedListNodesIterator<N> {
+ inner: AstSeparatedListElementsIterator<N>,
+}
+
+impl<N: AstNode> Iterator for AstSeparatedListNodesIterator<N> {
+ type Item = N;
+ fn next(&mut self) -> Option<Self::Item> {
+ Some(self.inner.next()?.node)
+ }
+}
+
+impl<N: AstNode> FusedIterator for AstSeparatedListNodesIterator<N> {}
+
+impl<N: AstNode> IntoIterator for AstSeparatedList<N> {
+ type Item = N;
+ type IntoIter = AstSeparatedListNodesIterator<N>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+impl<N: AstNode> IntoIterator for &AstSeparatedList<N> {
+ type Item = N;
+ type IntoIter = AstSeparatedListNodesIterator<N>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+/// Specific result used when navigating nodes using AST APIs
+pub type SyntaxResult<ResultType> = Result<ResultType, SyntaxError>;
+
+#[derive(Debug)]
+pub enum SyntaxError {
+ /// Error thrown when a mandatory node is not found
+ MissingRequiredChild(SyntaxNode),
+}
+
mod support {
- use super::{AstNode, AstNodeList, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken};
+ use super::{
+ AstNode, AstNodeList, AstSeparatedList, SyntaxElementChildren, SyntaxKind, SyntaxNode,
+ SyntaxToken,
+ };
use crate::ast::AstChildren;
use crate::SyntaxList;
use crate::{SyntaxError, SyntaxResult};
@@ -222,6 +395,13 @@ mod support {
AstNodeList::new(nth_syntax_list(parent, index))
}
+ pub(super) fn separated_list<N: AstNode>(
+ parent: &SyntaxNode,
+ index: usize,
+ ) -> AstSeparatedList<N> {
+ AstSeparatedList::new(nth_syntax_list(parent, index))
+ }
+
pub(super) fn token(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
parent
.children_with_tokens()
@@ -269,11 +449,173 @@ mod support {
}
}
-/// Specific result used when navigating nodes using AST APIs
-pub type SyntaxResult<ResultType> = Result<ResultType, SyntaxError>;
+#[cfg(test)]
+mod tests {
+ use crate::ast::{AstSeparatedElement, AstSeparatedList, Literal};
+ use crate::{JsLanguage, SyntaxKind};
+ use rome_rowan::TreeBuilder;
-#[derive(Debug)]
-pub enum SyntaxError {
- /// Error thrown when a mandatory node is not found
- MissingRequiredChild(SyntaxNode),
+ /// Creates a ast separated list over a sequence of numbers separated by ",".
+ /// The elements are pairs of: (value, separator).
+ fn build_list<'a>(
+ elements: impl IntoIterator<Item = (Option<i32>, Option<&'a str>)>,
+ ) -> AstSeparatedList<Literal> {
+ let mut builder: TreeBuilder<JsLanguage> = TreeBuilder::new();
+
+ builder.start_node(SyntaxKind::LIST);
+
+ for (node, separator) in elements.into_iter() {
+ if let Some(node) = node {
+ builder.start_node(SyntaxKind::LITERAL);
+ builder.token(SyntaxKind::NUMBER, node.to_string().as_str());
+ builder.finish_node();
+ }
+
+ if let Some(separator) = separator {
+ builder.token(SyntaxKind::COMMA, separator);
+ }
+ }
+
+ builder.finish_node();
+
+ let node = builder.finish();
+
+ AstSeparatedList::new(node.into_list().unwrap())
+ }
+
+ fn assert_elements<'a>(
+ actual: impl Iterator<Item = AstSeparatedElement<Literal>>,
+ expected: impl IntoIterator<Item = (f64, Option<&'a str>)>,
+ ) {
+ let actual = actual.map(|element| {
+ (
+ element.node.as_number().unwrap(),
+ element
+ .trailing_separator
+ .map(|separator| separator.text().to_string()),
+ )
+ });
+
+ let expected = expected
+ .into_iter()
+ .map(|(value, separator)| (value, separator.map(|sep| sep.to_string())))
+ .collect::<Vec<_>>();
+
+ assert_eq!(actual.collect::<Vec<_>>(), expected);
+ }
+
+ fn assert_nodes(
+ actual: impl Iterator<Item = Literal>,
+ expected: impl IntoIterator<Item = f64>,
+ ) {
+ assert_eq!(
+ actual
+ .map(|literal| literal.as_number().unwrap())
+ .collect::<Vec<_>>(),
+ expected.into_iter().collect::<Vec<_>>()
+ );
+ }
+
+ #[test]
+ fn empty() {
+ let list = build_list(vec![]);
+
+ assert_eq!(list.len(), 0);
+ assert!(list.is_empty());
+ assert_eq!(list.separators().count(), 0);
+
+ assert_nodes(list.iter(), vec![]);
+ assert_elements(list.elements(), vec![]);
+ assert_eq!(list.trailing_separator(), None);
+ }
+
+ #[test]
+ fn separated_list() {
+ let list = build_list(vec![
+ (Some(1), Some(",")),
+ (Some(2), Some(",")),
+ (Some(3), Some(",")),
+ (Some(4), None),
+ ]);
+
+ assert_eq!(list.len(), 4);
+ assert!(!list.is_empty());
+ assert_eq!(list.separators().count(), 3);
+
+ assert_nodes(list.iter(), vec![1., 2., 3., 4.]);
+ assert_elements(
+ list.elements(),
+ vec![
+ (1., Some(",")),
+ (2., Some(",")),
+ (3., Some(",")),
+ (4., None),
+ ],
+ );
+ assert_eq!(list.trailing_separator(), None);
+ }
+
+ #[test]
+ fn separated_with_trailing() {
+ // list(1, 2, 3, 4,)
+ let list = build_list(vec![
+ (Some(1), Some(",")),
+ (Some(2), Some(",")),
+ (Some(3), Some(",")),
+ (Some(4), Some(",")),
+ ]);
+
+ assert_eq!(list.len(), 4);
+ assert!(!list.is_empty());
+ assert_nodes(list.iter(), vec![1., 2., 3., 4.]);
+ assert_eq!(list.separators().count(), 4);
+
+ assert_elements(
+ list.elements(),
+ vec![
+ (1., Some(",")),
+ (2., Some(",")),
+ (3., Some(",")),
+ (4., Some(",")),
+ ],
+ );
+ assert!(list.trailing_separator().is_some());
+ }
+
+ #[test]
+ #[should_panic(
+ expected = "Missing element in separated list, found COMMA@2..3 \",\" token instead"
+ )]
+ fn separated_with_two_successive_separators() {
+ // list([1,,])
+ let list = build_list(vec![(Some(1), Some(",")), (None, Some(","))]);
+
+ // This should panic because having two successive separators is invalid.
+ // Grammars should instead model a "hole" node if this is a valid language construct.
+ let _ = list.elements().collect::<Vec<_>>();
+ }
+
+ #[test]
+ #[should_panic(
+ expected = "Missing element in separated list, found COMMA@0..1 \",\" token instead"
+ )]
+ fn separated_with_leading_separator() {
+ // list([,3])
+ let list = build_list(vec![(None, Some(",")), (Some(3), None)]);
+
+ // This should panic because the first element is a separator instead of a node.
+ let _ = list.elements().collect::<Vec<_>>();
+ }
+
+ #[test]
+ #[should_panic(
+ expected = "Expected separator but found node. Two nodes must always be separated by a separator token."
+ )]
+ fn separated_with_two_successive_nodes() {
+ // list([1 2,])
+ let list = build_list(vec![(Some(1), None), (Some(2), Some(","))]);
+
+ // This should panic because having two successive nodes is invalid.
+ let _ = list.elements().collect::<Vec<_>>();
+ }
}

MODIFY
crates/rslint_parser/src/ast/expr_ext.rs
crates/rslint_parser/src/ast/expr_ext.rs
@@ -382,16 +382,7 @@ impl ExprOrSpread {
impl ObjectExpr {
pub fn has_trailing_comma(&self) -> bool {
- if let Some(last) = self.props().last().map(|it| it.syntax().to_owned()) {
- if let Some(tok) = last
- .next_sibling_or_token()
- .map(|it| it.into_token())
- .flatten()
- {
- return tok.kind() == T![,];
- }
- }
- false
+ self.props().trailing_separator().is_some()
}
}

MODIFY
crates/rslint_parser/src/ast/generated/nodes.rs
crates/rslint_parser/src/ast/generated/nodes.rs
@@ -378,7 +378,9 @@ impl VarDecl {
pub fn const_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T![const])
}
- pub fn declared(&self) -> AstNodeList<Declarator> { support::node_list(&self.syntax, 0usize) }
+ pub fn declared(&self) -> AstSeparatedList<Declarator> {
+ support::separated_list(&self.syntax, 0usize)
+ }
pub fn semicolon_token(&self) -> Option<SyntaxToken> {
support::as_optional_token(&self.syntax, T ! [;])
}
@@ -536,7 +538,9 @@ impl ObjectExpr {
pub fn l_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['{'])
}
- pub fn props(&self) -> AstNodeList<ObjectProp> { support::node_list(&self.syntax, 0usize) }
+ pub fn props(&self) -> AstSeparatedList<ObjectProp> {
+ support::separated_list(&self.syntax, 0usize)
+ }
pub fn r_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['}'])
}
@@ -713,7 +717,7 @@ pub struct SequenceExpr {
pub(crate) syntax: SyntaxNode,
}
impl SequenceExpr {
- pub fn exprs(&self) -> AstNodeList<Expr> { support::node_list(&self.syntax, 0usize) }
+ pub fn exprs(&self) -> AstSeparatedList<Expr> { support::separated_list(&self.syntax, 0usize) }
pub fn bin_expr(&self) -> SyntaxResult<BinExpr> { support::as_mandatory_node(&self.syntax) }
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -908,7 +912,7 @@ impl ArgList {
pub fn l_paren_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['('])
}
- pub fn args(&self) -> AstNodeList<Expr> { support::node_list(&self.syntax, 0usize) }
+ pub fn args(&self) -> AstSeparatedList<Expr> { support::separated_list(&self.syntax, 0usize) }
pub fn r_paren_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T![')'])
}
@@ -934,7 +938,9 @@ impl ParameterList {
pub fn l_paren_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['('])
}
- pub fn parameters(&self) -> AstNodeList<Pattern> { support::node_list(&self.syntax, 0usize) }
+ pub fn parameters(&self) -> AstSeparatedList<Pattern> {
+ support::separated_list(&self.syntax, 0usize)
+ }
pub fn r_paren_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T![')'])
}
@@ -1113,8 +1119,8 @@ impl ConstructorParameters {
pub fn l_paren_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['('])
}
- pub fn parameters(&self) -> AstNodeList<ConstructorParamOrPat> {
- support::node_list(&self.syntax, 0usize)
+ pub fn parameters(&self) -> AstSeparatedList<ConstructorParamOrPat> {
+ support::separated_list(&self.syntax, 0usize)
}
pub fn r_paren_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T![')'])
@@ -1208,8 +1214,8 @@ impl ObjectPattern {
pub fn l_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['{'])
}
- pub fn elements(&self) -> AstNodeList<ObjectPatternProp> {
- support::node_list(&self.syntax, 0usize)
+ pub fn elements(&self) -> AstSeparatedList<ObjectPatternProp> {
+ support::separated_list(&self.syntax, 0usize)
}
pub fn r_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['}'])
@@ -1510,7 +1516,9 @@ impl ExportNamed {
pub fn l_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['{'])
}
- pub fn specifiers(&self) -> AstNodeList<Specifier> { support::node_list(&self.syntax, 0usize) }
+ pub fn specifiers(&self) -> AstSeparatedList<Specifier> {
+ support::separated_list(&self.syntax, 0usize)
+ }
pub fn r_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['}'])
}
@@ -1658,7 +1666,9 @@ impl NamedImports {
pub fn l_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['{'])
}
- pub fn specifiers(&self) -> AstNodeList<Specifier> { support::node_list(&self.syntax, 0usize) }
+ pub fn specifiers(&self) -> AstSeparatedList<Specifier> {
+ support::separated_list(&self.syntax, 0usize)
+ }
pub fn r_curly_token(&self) -> SyntaxResult<SyntaxToken> {
support::as_mandatory_token(&self.syntax, T!['}'])
}

MODIFY
crates/rslint_parser/src/lib.rs
crates/rslint_parser/src/lib.rs
@@ -77,7 +77,7 @@ pub mod syntax;
pub mod util;
pub use crate::{
- ast::{AstNode, AstNodeList, AstToken, SyntaxError, SyntaxResult},
+ ast::{AstNode, AstNodeList, AstSeparatedList, AstToken, SyntaxError, SyntaxResult},
event::{process, Event},
lossless_tree_sink::LosslessTreeSink,
lossy_tree_sink::LossyTreeSink,

MODIFY
crates/rslint_parser/src/syntax/util.rs
crates/rslint_parser/src/syntax/util.rs
@@ -268,19 +268,9 @@ pub fn check_for_stmt_lhs(p: &mut Parser, expr: Expr, marker: &CompletedMarker)
}
}
Expr::ObjectExpr(expr) => {
- // TODO replace with expr.props().trailing_comma()
- if expr.has_trailing_comma() {
+ if let Some(trailing_comma) = expr.props().trailing_separator() {
// Untyped node machine go brr
- let comma_range = expr
- .props()
- .last()
- .unwrap()
- .syntax()
- .next_sibling_or_token()
- .unwrap()
- .into_token()
- .unwrap()
- .text_range();
+ let comma_range = trailing_comma.text_range();
let err = p
.err_builder("Illegal trailing comma in assignment target")
.primary(comma_range, "");

MODIFY
xtask/js.ungram
xtask/js.ungram
// A B -- sequence
// A | B -- alternation
// A* -- zero or more repetition
+// (A (',' A)* ','?) -- repetition of node A separated by ',' and allowing a trailing comma
+// (A (',' A)*) -- repetition of node A separated by ',' without a trailing comma
// A? -- zero or one repetition
// (A) -- same as A
// label:A -- suggested name for field of AST node
@@ -273,7 +275,7 @@ ArrowExprParams =
| ParameterList
// Object expression
-ObjectExpr = '{' props:ObjectProp* '}'
+ObjectExpr = '{' props:(ObjectProp (',' ObjectProp)* ','?) '}'
// class expression
ClassExpr =
@@ -309,7 +311,7 @@ Constructor =
ConstructorParameters =
- '(' parameters:ConstructorParamOrPat* ')'
+ '(' parameters:(ConstructorParamOrPat (',' ConstructorParamOrPat)* ','?) ')'
ConstructorParamOrPat =
@@ -360,8 +362,7 @@ PatternOrExpr = Pattern | Expr
// sequence expression
SequenceExpr =
- // @ematipico correct grammar should be "exprs: (Expr (',' Expr)*)"
- exprs:Expr*
+ exprs:(Expr (',' Expr)*)
BinExpr
@@ -410,8 +411,8 @@ ExprOrBlock =
Expr
| BlockStmt
-ParameterList = '(' parameters:Pattern* ')'
-ArgList = '(' args:Expr* ')'
+ParameterList = '(' parameters:(Pattern (',' Pattern)* ','?) ')'
+ArgList = '(' args:(Expr (',' Expr)* ','?) ')'
///////////////
@@ -438,7 +439,7 @@ RestPattern = '...' pat:Pattern
ArrayPattern = '[' elements:Pattern* ']' '!' ':' ty:TsType
// object pattern
-ObjectPattern = '{' elements:ObjectPatternProp* '}'
+ObjectPattern = '{' elements:(ObjectPatternProp (',' ObjectPatternProp)* ','?) '}'
// object pattern prop
ObjectPatternProp =
@@ -555,7 +556,7 @@ FnDecl =
return_type:TsType?
body:BlockStmt
-VarDecl = ('var' | 'const' | manual__:'let') declared:Declarator* ';'?
+VarDecl = ('var' | 'const' | manual__:'let') declared:(Declarator (',' Declarator)*) ';'?
// @ematipico: should we consider ClassDecl = ClassExpr ?
ClassDecl = 'class' Name ('extends' parent:NameRef)? body:ClassBody
@@ -621,7 +622,7 @@ ImportClause =
| ImportStringSpecifier
-NamedImports = '{' specifiers:Specifier* '}'
+NamedImports = '{' specifiers:(Specifier (',' Specifier)* ','?) '}'
Specifier = name:Ident manual__:'as'? manual__alias:Name
@@ -633,8 +634,8 @@ ImportStringSpecifier = 'string'
WildcardImport = '*' 'as'? Ident?
-// @ematipico this one is not entirly correct I think..
-ExportNamed = 'export' 'type'? 'from'? '{' specifiers:Specifier* '}'
+// @ematipico this one is not entirely correct I think..
+ExportNamed = 'export' 'type'? 'from'? '{' specifiers:(Specifier (',' Specifier)* ','?) * '}'
///////////////
// TYPESCRIPT

MODIFY
xtask/src/codegen/ast.rs
xtask/src/codegen/ast.rs
@@ -135,6 +135,7 @@ fn handle_rule(
ty,
optional,
has_many,
+ separated: false,
};
fields.push(field);
}
@@ -157,12 +158,82 @@ fn handle_rule(
Rule::Opt(rule) => {
handle_rule(fields, grammar, rule, label, true, false);
}
- Rule::Seq(rules) | Rule::Alt(rules) => {
+ Rule::Alt(rules) => {
+ for rule in rules {
+ handle_rule(fields, grammar, rule, label, false, false);
+ }
+ }
+
+ Rule::Seq(rules) => {
+ if handle_comma_list(fields, grammar, label, rules.as_slice()) {
+ return;
+ }
+
for rule in rules {
handle_rule(fields, grammar, rule, label, false, false);
}
}
};
+
+ // (T (',' T)* ','?)
+ // (T (',' T)*)
+ fn handle_comma_list(
+ acc: &mut Vec<Field>,
+ grammar: &Grammar,
+ label: Option<&String>,
+ rules: &[Rule],
+ ) -> bool {
+ // Does it match (T * ',')?
+ let (node, repeat, trailing_separator) = match rules {
+ [Rule::Node(node), Rule::Rep(repeat), Rule::Opt(trailing_separator)] => {
+ (node, repeat, Some(trailing_separator))
+ }
+ [Rule::Node(node), Rule::Rep(repeat)] => (node, repeat, None),
+ _ => return false,
+ };
+
+ // Is the repeat a ()*?
+ let repeat = match &**repeat {
+ Rule::Seq(it) => it,
+ _ => return false,
+ };
+
+ // Does the repeat match (token node))
+ match repeat.as_slice() {
+ [comma, Rule::Node(n)] => {
+ let separator_matches_trailing = if let Some(trailing) = trailing_separator {
+ &**trailing == comma
+ } else {
+ true
+ };
+
+ if n != node || !separator_matches_trailing {
+ return false;
+ }
+ }
+ _ => return false,
+ }
+
+ let ty = grammar[*node].name.clone();
+
+ let name = label
+ .cloned()
+ .unwrap_or_else(|| pluralize(&to_lower_snake_case(&ty)));
+
+ acc.push(Field::Node {
+ name,
+ ty,
+ optional: false,
+ has_many: true,
+ separated: true,
+ });
+
+ true
+ }
+
+ fn pluralize(s: &str) -> String {
+ format!("{}s", s)
+ }
}
// handle cases like: `op: ('-' | '+' | '*')`

MODIFY
xtask/src/codegen/generate_nodes.rs
xtask/src/codegen/generate_nodes.rs
@@ -72,6 +72,7 @@ pub fn generate_nodes(ast: &AstSrc) -> Result<String> {
ty,
optional,
has_many,
+ separated,
} => {
let is_built_in_tpe = &ty.eq(BUILT_IN_TYPE);
let ty = format_ident!("{}", &ty);
@@ -93,11 +94,20 @@ pub fn generate_nodes(ast: &AstSrc) -> Result<String> {
}
}
} else if *has_many {
- let field = quote! {
+ let field = if *separated {
+ quote! {
+ pub fn #method_name(&self) -> AstSeparatedList<#ty> {
+ support::separated_list(&self.syntax, #slot)
+ }
+ }
+ } else {
+ quote! {
pub fn #method_name(&self) -> AstNodeList<#ty> {
support::node_list(&self.syntax, #slot)
}
+ }
};
+
slot += 1;
field
} else {

MODIFY
xtask/src/codegen/kinds_src.rs
xtask/src/codegen/kinds_src.rs
@@ -379,6 +379,7 @@ pub enum Field {
ty: String,
optional: bool,
has_many: bool,
+ separated: bool,
},
}

feat(parser): AST Separated List (#1746)

Implements an AST view for lists that contain nodes separated by tokens, for example, an array expression where the elements are separated by commas.