diff --git a/Cargo.lock b/Cargo.lock index 57f5c864b..1dc654460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,9 +220,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "chrono" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1159,7 +1160,7 @@ dependencies = [ "blake2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "caseless 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1209,6 +1210,7 @@ dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode_categories 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "unicode_names2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", "wtf8 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2033,7 +2035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum caseless 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "808dab3318747be122cb31d36de18d4d1c81277a76f8332a02b81a3d73463d7f" "checksum cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)" = "4fc9a35e1f4290eb9e5fc54ba6cf40671ed2a2514c3eeb2b2a908dda2ea5a1be" "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" -"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum chrono 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "77d81f58b7301084de3b958691458a53c3f7e0b1d702f77e550b6a88e3a88abe" "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" diff --git a/parser/src/ast.rs b/parser/src/ast.rs index d56169fd0..40c28591e 100644 --- a/parser/src/ast.rs +++ b/parser/src/ast.rs @@ -321,6 +321,12 @@ pub struct Comprehension { pub is_async: bool, } +#[derive(Debug, PartialEq)] +pub struct ArgumentList { + pub args: Vec, + pub keywords: Vec, +} + #[derive(Debug, PartialEq)] pub struct Keyword { pub name: Option, diff --git a/parser/src/error.rs b/parser/src/error.rs index f0d4c8bcd..9a8865710 100644 --- a/parser/src/error.rs +++ b/parser/src/error.rs @@ -20,6 +20,7 @@ pub enum LexicalErrorType { StringError, UnicodeError, NestingError, + PositionalArgumentError, UnrecognizedToken { tok: char }, FStringError(FStringErrorType), OtherError(String), @@ -32,6 +33,9 @@ impl fmt::Display for LexicalErrorType { LexicalErrorType::FStringError(error) => write!(f, "Got error in f-string: {}", error), LexicalErrorType::UnicodeError => write!(f, "Got unexpected unicode"), LexicalErrorType::NestingError => write!(f, "Got unexpected nesting"), + LexicalErrorType::PositionalArgumentError => { + write!(f, "positional argument follows keyword argument") + } LexicalErrorType::UnrecognizedToken { tok } => { write!(f, "Got unexpected token {}", tok) } @@ -40,6 +44,12 @@ impl fmt::Display for LexicalErrorType { } } +impl From for LalrpopError { + fn from(err: LexicalError) -> Self { + lalrpop_util::ParseError::User { error: err } + } +} + // TODO: consolidate these with ParseError #[derive(Debug, PartialEq)] pub struct FStringError { diff --git a/parser/src/function.rs b/parser/src/function.rs new file mode 100644 index 000000000..552bdfd10 --- /dev/null +++ b/parser/src/function.rs @@ -0,0 +1,36 @@ +use crate::ast; +use crate::error::{LexicalError, LexicalErrorType}; + +type FunctionArgument = (Option>, ast::Expression); + +pub fn parse_args(func_args: Vec) -> Result { + let mut args = vec![]; + let mut keywords = vec![]; + for (name, value) in func_args { + match name { + Some(n) => { + keywords.push(ast::Keyword { name: n, value }); + } + None => { + // Allow starred args after keyword arguments. + if !keywords.is_empty() && !is_starred(&value) { + return Err(LexicalError { + error: LexicalErrorType::PositionalArgumentError, + location: value.location.clone(), + }); + } + + args.push(value); + } + } + } + Ok(ast::ArgumentList { args, keywords }) +} + +fn is_starred(exp: &ast::Expression) -> bool { + if let ast::ExpressionType::Starred { .. } = exp.node { + true + } else { + false + } +} diff --git a/parser/src/lib.rs b/parser/src/lib.rs index 0e98924b4..01da4152b 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -8,6 +8,7 @@ use lalrpop_util::lalrpop_mod; pub mod ast; pub mod error; mod fstring; +mod function; pub mod lexer; pub mod location; pub mod parser; diff --git a/parser/src/python.lalrpop b/parser/src/python.lalrpop index 2ee1e90d9..7e3cc6200 100644 --- a/parser/src/python.lalrpop +++ b/parser/src/python.lalrpop @@ -7,6 +7,7 @@ use std::iter::FromIterator; use crate::ast; use crate::fstring::parse_located_fstring; +use crate::function::parse_args; use crate::error::LexicalError; use crate::lexer; use crate::location; @@ -580,7 +581,7 @@ KwargParameter: Option = { ClassDef: ast::Statement = { "class" ":" => { let (bases, keywords) = match a { - Some((_, args, _)) => args, + Some((_, arg, _)) => (arg.args, arg.keywords), None => (vec![], vec![]), }; ast::Statement { @@ -616,14 +617,13 @@ Path: ast::Expression = { Decorator: ast::Expression = { "@" "\n" => { match a { - Some((location, _, args, _)) => { - let (args, keywords) = args; + Some((location, _, arg, _)) => { ast::Expression { location, node: ast::ExpressionType::Call { function: Box::new(p), - args, - keywords, + args: arg.args, + keywords: arg.keywords, } } }, @@ -633,7 +633,7 @@ Decorator: ast::Expression = { }; YieldExpr: ast::Expression = { - "yield" => ast::Expression { + "yield" => ast::Expression { location, node: ast::ExpressionType::Yield { value: value.map(Box::new) } }, @@ -847,10 +847,9 @@ AtomExpr: ast::Expression = { AtomExpr2: ast::Expression = { Atom, "(" ")" => { - let (args, keywords) = a; ast::Expression { location, - node: ast::ExpressionType::Call { function: Box::new(f), args, keywords } + node: ast::ExpressionType::Call { function: Box::new(f), args: a.args, keywords: a.keywords } } }, "[" "]" => ast::Expression { @@ -1060,31 +1059,10 @@ SingleForComprehension: ast::Comprehension = { ExpressionNoCond: ast::Expression = OrTest; ComprehensionIf: ast::Expression = "if" => c; -ArgumentList: (Vec, Vec) = { - > => { - let mut args = vec![]; - let mut keywords = vec![]; - for (name, value) in e { - match name { - Some(n) => { - keywords.push(ast::Keyword { name: n, value: value }); - }, - None => { - // Allow starred args after keyword arguments. - let is_starred = if let ast::ExpressionType::Starred { .. } = &value.node { - true - } else { - false - }; - - if keywords.len() > 0 && !is_starred { - panic!("positional argument follows keyword argument {:?}", keywords); - }; - args.push(value); - }, - } - } - (args, keywords) +ArgumentList: ast::ArgumentList = { + > =>? { + let arg_list = parse_args(e)?; + Ok(arg_list) } }; diff --git a/tests/snippets/function.py b/tests/snippets/function.py index 59fd028ea..c80ab7ea1 100644 --- a/tests/snippets/function.py +++ b/tests/snippets/function.py @@ -1,3 +1,6 @@ +from testutils import assertRaises + + __name__ = "function" @@ -88,3 +91,7 @@ def f8() -> int: return 10 assert f8() == 10 + + +with assertRaises(SyntaxError): + exec('print(keyword=10, 20)')