Skip to content

Commit 4438350

Browse files
authored
Merge pull request RustPython#4490 from DimitrisJim/function_parser
Add tests, some comments, to function.rs.
2 parents d3855a9 + bc9e4ab commit 4438350

16 files changed

+2265
-7
lines changed

compiler/parser/src/function.rs

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
// Contains functions that perform validation and parsing of arguments and parameters.
2+
// Checks apply both to functions and to lambdas.
13
use crate::ast;
24
use crate::error::{LexicalError, LexicalErrorType};
35
use rustc_hash::FxHashSet;
46

5-
pub struct ArgumentList {
7+
pub(crate) struct ArgumentList {
68
pub args: Vec<ast::Expr>,
79
pub keywords: Vec<ast::Keyword>,
810
}
911

1012
type ParameterDefs = (Vec<ast::Arg>, Vec<ast::Arg>, Vec<ast::Expr>);
1113
type ParameterDef = (ast::Arg, Option<ast::Expr>);
1214

13-
pub fn validate_arguments(arguments: ast::Arguments) -> Result<ast::Arguments, LexicalError> {
15+
// Perform validation of function/lambda arguments in a function definition.
16+
pub(crate) fn validate_arguments(
17+
arguments: ast::Arguments,
18+
) -> Result<ast::Arguments, LexicalError> {
1419
let mut all_args: Vec<&ast::Located<ast::ArgData>> = vec![];
1520

1621
all_args.extend(arguments.posonlyargs.iter());
@@ -29,6 +34,7 @@ pub fn validate_arguments(arguments: ast::Arguments) -> Result<ast::Arguments, L
2934
let mut all_arg_names = FxHashSet::with_hasher(Default::default());
3035
for arg in all_args {
3136
let arg_name = &arg.node.arg;
37+
// Check for duplicate arguments in the function definition.
3238
if !all_arg_names.insert(arg_name) {
3339
return Err(LexicalError {
3440
error: LexicalErrorType::DuplicateArgumentError(arg_name.to_string()),
@@ -40,7 +46,8 @@ pub fn validate_arguments(arguments: ast::Arguments) -> Result<ast::Arguments, L
4046
Ok(arguments)
4147
}
4248

43-
pub fn parse_params(
49+
// Parse parameters as supplied during a function/lambda *definition*.
50+
pub(crate) fn parse_params(
4451
params: (Vec<ParameterDef>, Vec<ParameterDef>),
4552
) -> Result<ParameterDefs, LexicalError> {
4653
let mut pos_only = Vec::with_capacity(params.0.len());
@@ -52,7 +59,7 @@ pub fn parse_params(
5259
defaults.push(default);
5360
} else if !defaults.is_empty() {
5461
// Once we have started with defaults, all remaining arguments must
55-
// have defaults
62+
// have defaults.
5663
return Err(LexicalError {
5764
error: LexicalErrorType::DefaultArgumentError,
5865
location: name.location,
@@ -79,7 +86,8 @@ type FunctionArgument = (
7986
ast::Expr,
8087
);
8188

82-
pub fn parse_args(func_args: Vec<FunctionArgument>) -> Result<ArgumentList, LexicalError> {
89+
// Parse arguments as supplied during a function/lambda *call*.
90+
pub(crate) fn parse_args(func_args: Vec<FunctionArgument>) -> Result<ArgumentList, LexicalError> {
8391
let mut args = vec![];
8492
let mut keywords = vec![];
8593

@@ -89,6 +97,7 @@ pub fn parse_args(func_args: Vec<FunctionArgument>) -> Result<ArgumentList, Lexi
8997
for (name, value) in func_args {
9098
match name {
9199
Some((start, end, name)) => {
100+
// Check for duplicate keyword arguments in the call.
92101
if let Some(keyword_name) = &name {
93102
if keyword_names.contains(keyword_name) {
94103
return Err(LexicalError {
@@ -111,13 +120,14 @@ pub fn parse_args(func_args: Vec<FunctionArgument>) -> Result<ArgumentList, Lexi
111120
));
112121
}
113122
None => {
114-
// Allow starred arguments after keyword arguments but
115-
// not after double-starred arguments.
123+
// Positional arguments mustn't follow keyword arguments.
116124
if !keywords.is_empty() && !is_starred(&value) {
117125
return Err(LexicalError {
118126
error: LexicalErrorType::PositionalArgumentError,
119127
location: value.location,
120128
});
129+
// Allow starred arguments after keyword arguments but
130+
// not after double-starred arguments.
121131
} else if double_starred {
122132
return Err(LexicalError {
123133
error: LexicalErrorType::UnpackedArgumentError,
@@ -132,6 +142,86 @@ pub fn parse_args(func_args: Vec<FunctionArgument>) -> Result<ArgumentList, Lexi
132142
Ok(ArgumentList { args, keywords })
133143
}
134144

145+
// Check if an expression is a starred expression.
135146
fn is_starred(exp: &ast::Expr) -> bool {
136147
matches!(exp.node, ast::ExprKind::Starred { .. })
137148
}
149+
150+
#[cfg(test)]
151+
mod tests {
152+
use crate::error::{LexicalErrorType, ParseErrorType};
153+
use crate::parser::parse_program;
154+
155+
macro_rules! function_and_lambda {
156+
($($name:ident: $code:expr,)*) => {
157+
$(
158+
#[test]
159+
fn $name() {
160+
let parse_ast = parse_program($code, "<test>");
161+
insta::assert_debug_snapshot!(parse_ast);
162+
}
163+
)*
164+
}
165+
}
166+
167+
function_and_lambda! {
168+
test_function_no_args: "def f(): pass",
169+
test_function_pos_args: "def f(a, b, c): pass",
170+
test_function_pos_args_with_defaults: "def f(a, b=20, c=30): pass",
171+
test_function_kw_only_args: "def f(*, a, b, c): pass",
172+
test_function_kw_only_args_with_defaults: "def f(*, a, b=20, c=30): pass",
173+
test_function_pos_and_kw_only_args: "def f(a, b, c, *, d, e, f): pass",
174+
test_function_pos_and_kw_only_args_with_defaults: "def f(a, b, c, *, d, e=20, f=30): pass",
175+
test_function_pos_and_kw_only_args_with_defaults_and_varargs: "def f(a, b, c, *args, d, e=20, f=30): pass",
176+
test_function_pos_and_kw_only_args_with_defaults_and_varargs_and_kwargs: "def f(a, b, c, *args, d, e=20, f=30, **kwargs): pass",
177+
test_lambda_no_args: "lambda: 1",
178+
test_lambda_pos_args: "lambda a, b, c: 1",
179+
test_lambda_pos_args_with_defaults: "lambda a, b=20, c=30: 1",
180+
test_lambda_kw_only_args: "lambda *, a, b, c: 1",
181+
test_lambda_kw_only_args_with_defaults: "lambda *, a, b=20, c=30: 1",
182+
test_lambda_pos_and_kw_only_args: "lambda a, b, c, *, d, e: 0",
183+
}
184+
185+
fn function_parse_error(src: &str) -> LexicalErrorType {
186+
let parse_ast = parse_program(src, "<test>");
187+
parse_ast
188+
.map_err(|e| match e.error {
189+
ParseErrorType::Lexical(e) => e,
190+
_ => panic!("Expected LexicalError"),
191+
})
192+
.expect_err("Expected error")
193+
}
194+
195+
macro_rules! function_and_lambda_error {
196+
($($name:ident: $code:expr, $error:expr,)*) => {
197+
$(
198+
#[test]
199+
fn $name() {
200+
let error = function_parse_error($code);
201+
assert_eq!(error, $error);
202+
}
203+
)*
204+
}
205+
}
206+
207+
function_and_lambda_error! {
208+
// Check definitions
209+
test_duplicates_f1: "def f(a, a): pass", LexicalErrorType::DuplicateArgumentError("a".to_string()),
210+
test_duplicates_f2: "def f(a, *, a): pass", LexicalErrorType::DuplicateArgumentError("a".to_string()),
211+
test_duplicates_f3: "def f(a, a=20): pass", LexicalErrorType::DuplicateArgumentError("a".to_string()),
212+
test_duplicates_f4: "def f(a, *a): pass", LexicalErrorType::DuplicateArgumentError("a".to_string()),
213+
test_duplicates_f5: "def f(a, *, **a): pass", LexicalErrorType::DuplicateArgumentError("a".to_string()),
214+
test_duplicates_l1: "lambda a, a: 1", LexicalErrorType::DuplicateArgumentError("a".to_string()),
215+
test_duplicates_l2: "lambda a, *, a: 1", LexicalErrorType::DuplicateArgumentError("a".to_string()),
216+
test_duplicates_l3: "lambda a, a=20: 1", LexicalErrorType::DuplicateArgumentError("a".to_string()),
217+
test_duplicates_l4: "lambda a, *a: 1", LexicalErrorType::DuplicateArgumentError("a".to_string()),
218+
test_duplicates_l5: "lambda a, *, **a: 1", LexicalErrorType::DuplicateArgumentError("a".to_string()),
219+
test_default_arg_error_f: "def f(a, b=20, c): pass", LexicalErrorType::DefaultArgumentError,
220+
test_default_arg_error_l: "lambda a, b=20, c: 1", LexicalErrorType::DefaultArgumentError,
221+
222+
// Check some calls.
223+
test_positional_arg_error_f: "f(b=20, c)", LexicalErrorType::PositionalArgumentError,
224+
test_unpacked_arg_error_f: "f(**b, *c)", LexicalErrorType::UnpackedArgumentError,
225+
test_duplicate_kw_f1: "f(a=20, a=30)", LexicalErrorType::DuplicateKeywordArgumentError("a".to_string()),
226+
}
227+
}

compiler/parser/src/snapshots/rustpython_parser__function__tests__function_kw_only_args.snap

Lines changed: 108 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)