Repository: dtolnay/syn
Pull request #1376 (2023-02-08 02:36:50)

Stricter parsing of ranges
None of the following are valid expressions, but were previously accepted by syn.

- `..=`
- `lo..=`
- `...`
- `...hi`
- `lo...`
- `lo...hi`

None of the following are valid patterns, but were previously accepted by syn.

- `[lo..]`
- `[..=hi]`


Patch:
diff --git a/src/expr.rs b/src/expr.rs
index 67ddb90c..67ee0a47 100644
--- a/src/expr.rs
+++ b/src/expr.rs
@@ -1199,11 +1199,12 @@ pub(crate) mod parsing {
                 });
             } else if Precedence::Range >= base && input.peek(Token![..]) {
                 let limits: RangeLimits = input.parse()?;
-                let rhs = if input.is_empty()
-                    || input.peek(Token![,])
-                    || input.peek(Token![;])
-                    || input.peek(Token![.]) && !input.peek(Token![..])
-                    || !allow_struct.0 && input.peek(token::Brace)
+                let rhs = if matches!(limits, RangeLimits::HalfOpen(_))
+                    && (input.is_empty()
+                        || input.peek(Token![,])
+                        || input.peek(Token![;])
+                        || input.peek(Token![.]) && !input.peek(Token![..])
+                        || !allow_struct.0 && input.peek(token::Brace))
                 {
                     None
                 } else {
@@ -2571,23 +2572,24 @@ pub(crate) mod parsing {
 
     #[cfg(feature = "full")]
     fn expr_range(input: ParseStream, allow_struct: AllowStruct) -> Result<ExprRange> {
+        let limits: RangeLimits = input.parse()?;
+        let end = if matches!(limits, RangeLimits::HalfOpen(_))
+            && (input.is_empty()
+                || input.peek(Token![,])
+                || input.peek(Token![;])
+                || input.peek(Token![.]) && !input.peek(Token![..])
+                || !allow_struct.0 && input.peek(token::Brace))
+        {
+            None
+        } else {
+            let to = ambiguous_expr(input, allow_struct)?;
+            Some(Box::new(to))
+        };
         Ok(ExprRange {
             attrs: Vec::new(),
             start: None,
-            limits: input.parse()?,
-            end: {
-                if input.is_empty()
-                    || input.peek(Token![,])
-                    || input.peek(Token![;])
-                    || input.peek(Token![.]) && !input.peek(Token![..])
-                    || !allow_struct.0 && input.peek(token::Brace)
-                {
-                    None
-                } else {
-                    let to = ambiguous_expr(input, allow_struct)?;
-                    Some(Box::new(to))
-                }
-            },
+            limits,
+            end,
         })
     }
 
@@ -2596,12 +2598,32 @@ pub(crate) mod parsing {
     impl Parse for RangeLimits {
         fn parse(input: ParseStream) -> Result<Self> {
             let lookahead = input.lookahead1();
-            if lookahead.peek(Token![..=]) {
+            let dot_dot = lookahead.peek(Token![..]);
+            let dot_dot_eq = dot_dot && lookahead.peek(Token![..=]);
+            let dot_dot_dot = dot_dot && input.peek(Token![...]);
+            if dot_dot_eq {
+                input.parse().map(RangeLimits::Closed)
+            } else if dot_dot && !dot_dot_dot {
+                input.parse().map(RangeLimits::HalfOpen)
+            } else {
+                Err(lookahead.error())
+            }
+        }
+    }
+
+    #[cfg(feature = "full")]
+    impl RangeLimits {
+        pub(crate) fn parse_obsolete(input: ParseStream) -> Result<Self> {
+            let lookahead = input.lookahead1();
+            let dot_dot = lookahead.peek(Token![..]);
+            let dot_dot_eq = dot_dot && lookahead.peek(Token![..=]);
+            let dot_dot_dot = dot_dot && input.peek(Token![...]);
+            if dot_dot_eq {
                 input.parse().map(RangeLimits::Closed)
-            } else if lookahead.peek(Token![...]) {
+            } else if dot_dot_dot {
                 let dot3: Token![...] = input.parse()?;
                 Ok(RangeLimits::Closed(Token![..=](dot3.spans)))
-            } else if lookahead.peek(Token![..]) {
+            } else if dot_dot {
                 input.parse().map(RangeLimits::HalfOpen)
             } else {
                 Err(lookahead.error())
diff --git a/src/pat.rs b/src/pat.rs
index 56336f82..1aaa2317 100644
--- a/src/pat.rs
+++ b/src/pat.rs
@@ -259,20 +259,14 @@ pub(crate) mod parsing {
         pub fn parse_single(input: ParseStream) -> Result<Self> {
             let begin = input.fork();
             let lookahead = input.lookahead1();
-            if {
-                let ahead = input.fork();
-                ahead.parse::<Option<Ident>>()?.is_some()
-                    && (ahead.peek(Token![::])
-                        || ahead.peek(Token![!])
-                        || ahead.peek(token::Brace)
-                        || ahead.peek(token::Paren)
-                        || ahead.peek(Token![..])
-                            && ahead.parse::<RangeLimits>().is_ok()
-                            && !(ahead.is_empty() || ahead.peek(Token![,])))
-            } || {
-                let ahead = input.fork();
-                ahead.parse::<Option<Token![self]>>()?.is_some() && ahead.peek(Token![::])
-            } || lookahead.peek(Token![::])
+            if lookahead.peek(Ident)
+                && (input.peek2(Token![::])
+                    || input.peek2(Token![!])
+                    || input.peek2(token::Brace)
+                    || input.peek2(token::Paren)
+                    || input.peek2(Token![..]))
+                || input.peek(Token![self]) && input.peek2(Token![::])
+                || lookahead.peek(Token![::])
                 || lookahead.peek(Token![<])
                 || input.peek(Token![Self])
                 || input.peek(Token![super])
@@ -578,8 +572,11 @@ pub(crate) mod parsing {
     }
 
     fn pat_range(input: ParseStream, qself: Option<QSelf>, path: Path) -> Result<Pat> {
-        let limits: RangeLimits = input.parse()?;
+        let limits = RangeLimits::parse_obsolete(input)?;
         let end = input.call(pat_range_bound)?;
+        if let (RangeLimits::Closed(_), None) = (&limits, &end) {
+            return Err(input.error("expected range upper bound"));
+        }
         Ok(Pat::Range(ExprRange {
             attrs: Vec::new(),
             start: Some(Box::new(Expr::Path(ExprPath {
@@ -655,8 +652,11 @@ pub(crate) mod parsing {
     fn pat_lit_or_range(input: ParseStream) -> Result<Pat> {
         let start = input.call(pat_range_bound)?.unwrap();
         if input.peek(Token![..]) {
-            let limits: RangeLimits = input.parse()?;
+            let limits = RangeLimits::parse_obsolete(input)?;
             let end = input.call(pat_range_bound)?;
+            if let (RangeLimits::Closed(_), None) = (&limits, &end) {
+                return Err(input.error("expected range upper bound"));
+            }
             Ok(Pat::Range(ExprRange {
                 attrs: Vec::new(),
                 start: Some(start.into_expr()),
@@ -733,6 +733,19 @@ pub(crate) mod parsing {
         let mut elems = Punctuated::new();
         while !content.is_empty() {
             let value = Pat::parse_multi_with_leading_vert(&content)?;
+            match value {
+                Pat::Range(pat) if pat.start.is_none() || pat.end.is_none() => {
+                    let (start, end) = match pat.limits {
+                        RangeLimits::HalfOpen(dot_dot) => (dot_dot.spans[0], dot_dot.spans[1]),
+                        RangeLimits::Closed(dot_dot_eq) => {
+                            (dot_dot_eq.spans[0], dot_dot_eq.spans[2])
+                        }
+                    };
+                    let msg = "range pattern is not allowed unparenthesized inside slice pattern";
+                    return Err(error::new2(start, end, msg));
+                }
+                _ => {}
+            }
             elems.push_value(value);
             if content.is_empty() {
                 break;


Test patch:
diff --git a/tests/repo/mod.rs b/tests/repo/mod.rs
index 03f2b857..75236224 100644
--- a/tests/repo/mod.rs
+++ b/tests/repo/mod.rs
@@ -50,9 +50,13 @@ static EXCLUDE_FILES: &[&str] = &[
     "src/tools/rustfmt/tests/source/type-ascription.rs",
     "src/tools/rustfmt/tests/target/type-ascription.rs",
 
+    // Invalid unparenthesized range pattern inside slice pattern: `[1..]`
+    "tests/ui/consts/miri_unleashed/const_refers_to_static_cross_crate.rs",
+
     // Various extensions to Rust syntax made up by rust-analyzer
     "src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/0012_type_item_where_clause.rs",
     "src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/0040_crate_keyword_vis.rs",
+    "src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/0058_range_pat.rs",
     "src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/0123_param_list_vararg.rs",
     "src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/0131_existential_type.rs",
     "src/tools/rust-analyzer/crates/parser/test_data/parser/inline/ok/0156_fn_def_param.rs",
@@ -64,6 +68,7 @@ static EXCLUDE_FILES: &[&str] = &[
     "src/tools/rust-analyzer/crates/parser/test_data/parser/ok/0055_dot_dot_dot.rs",
     "src/tools/rust-analyzer/crates/parser/test_data/parser/ok/0068_item_modifiers.rs",
     "src/tools/rust-analyzer/crates/syntax/test_data/parser/validation/0031_block_inner_attrs.rs",
+    "src/tools/rust-analyzer/crates/syntax/test_data/parser/validation/0038_endless_inclusive_range.rs",
     "src/tools/rust-analyzer/crates/syntax/test_data/parser/validation/0045_ambiguous_trait_object.rs",
     "src/tools/rust-analyzer/crates/syntax/test_data/parser/validation/0046_mutable_const_item.rs",
 
diff --git a/tests/test_expr.rs b/tests/test_expr.rs
index 5983ef96..c7230c6d 100644
--- a/tests/test_expr.rs
+++ b/tests/test_expr.rs
@@ -292,3 +292,21 @@ fn test_postfix_operator_after_cast() {
     syn::parse_str::<Expr>("|| &x as T[0]").unwrap_err();
     syn::parse_str::<Expr>("|| () as ()()").unwrap_err();
 }
+
+#[test]
+fn test_ranges() {
+    syn::parse_str::<Expr>("..").unwrap();
+    syn::parse_str::<Expr>("..hi").unwrap();
+    syn::parse_str::<Expr>("lo..").unwrap();
+    syn::parse_str::<Expr>("lo..hi").unwrap();
+
+    syn::parse_str::<Expr>("..=").unwrap_err();
+    syn::parse_str::<Expr>("..=hi").unwrap();
+    syn::parse_str::<Expr>("lo..=").unwrap_err();
+    syn::parse_str::<Expr>("lo..=hi").unwrap();
+
+    syn::parse_str::<Expr>("...").unwrap_err();
+    syn::parse_str::<Expr>("...hi").unwrap_err();
+    syn::parse_str::<Expr>("lo...").unwrap_err();
+    syn::parse_str::<Expr>("lo...hi").unwrap_err();
+}
diff --git a/tests/test_pat.rs b/tests/test_pat.rs
index ec19bd61..cab7aa7f 100644
--- a/tests/test_pat.rs
+++ b/tests/test_pat.rs
@@ -65,3 +65,33 @@ fn test_group() {
     }
     "###);
 }
+
+#[test]
+fn test_ranges() {
+    Pat::parse_single.parse_str("..").unwrap();
+    Pat::parse_single.parse_str("..hi").unwrap();
+    Pat::parse_single.parse_str("lo..").unwrap();
+    Pat::parse_single.parse_str("lo..hi").unwrap();
+
+    Pat::parse_single.parse_str("..=").unwrap_err();
+    Pat::parse_single.parse_str("..=hi").unwrap();
+    Pat::parse_single.parse_str("lo..=").unwrap_err();
+    Pat::parse_single.parse_str("lo..=hi").unwrap();
+
+    Pat::parse_single.parse_str("...").unwrap_err();
+    Pat::parse_single.parse_str("...hi").unwrap_err();
+    Pat::parse_single.parse_str("lo...").unwrap_err();
+    Pat::parse_single.parse_str("lo...hi").unwrap();
+
+    Pat::parse_single.parse_str("[lo..]").unwrap_err();
+    Pat::parse_single.parse_str("[..=hi]").unwrap_err();
+    Pat::parse_single.parse_str("[(lo..)]").unwrap();
+    Pat::parse_single.parse_str("[(..=hi)]").unwrap();
+    Pat::parse_single.parse_str("[lo..=hi]").unwrap();
+
+    Pat::parse_single.parse_str("[_, lo.., _]").unwrap_err();
+    Pat::parse_single.parse_str("[_, ..=hi, _]").unwrap_err();
+    Pat::parse_single.parse_str("[_, (lo..), _]").unwrap();
+    Pat::parse_single.parse_str("[_, (..=hi), _]").unwrap();
+    Pat::parse_single.parse_str("[_, lo..=hi, _]").unwrap();
+}