diff --git a/config.yml b/config.yml index 748729ec6b..248dd5a5ca 100644 --- a/config.yml +++ b/config.yml @@ -2032,7 +2032,8 @@ nodes: comment: | Represents reading a local variable. Note that this requires that a local variable of the same name has already been written to in the same scope, - otherwise it is parsed as a method call. + otherwise it is parsed as a method call. Note that `it` default parameter + has `0it` as the name of this node. foo ^^^ diff --git a/include/prism/diagnostic.h b/include/prism/diagnostic.h index da430b5438..fb41102361 100644 --- a/include/prism/diagnostic.h +++ b/include/prism/diagnostic.h @@ -167,6 +167,7 @@ typedef enum { PM_ERR_INVALID_PERCENT, PM_ERR_INVALID_TOKEN, PM_ERR_INVALID_VARIABLE_GLOBAL, + PM_ERR_IT_NOT_ALLOWED, PM_ERR_LAMBDA_OPEN, PM_ERR_LAMBDA_TERM_BRACE, PM_ERR_LAMBDA_TERM_END, diff --git a/src/diagnostic.c b/src/diagnostic.c index c779955eb3..ed47a1cdad 100644 --- a/src/diagnostic.c +++ b/src/diagnostic.c @@ -175,6 +175,7 @@ static const char* const diagnostic_messages[PM_DIAGNOSTIC_ID_LEN] = { [PM_ERR_INVALID_PERCENT] = "invalid `%` token", // TODO WHAT? [PM_ERR_INVALID_TOKEN] = "invalid token", // TODO WHAT? [PM_ERR_INVALID_VARIABLE_GLOBAL] = "invalid global variable", + [PM_ERR_IT_NOT_ALLOWED] = "`it` is not allowed when an ordinary parameter is defined", [PM_ERR_LAMBDA_OPEN] = "expected a `do` keyword or a `{` to open the lambda block", [PM_ERR_LAMBDA_TERM_BRACE] = "expected a lambda block beginning with `{` to end with `}`", [PM_ERR_LAMBDA_TERM_END] = "expected a lambda block beginning with `do` to end with `end`", diff --git a/src/prism.c b/src/prism.c index 570b86ab1d..67dcfcf451 100644 --- a/src/prism.c +++ b/src/prism.c @@ -4200,12 +4200,10 @@ pm_local_variable_or_write_node_create(pm_parser_t *parser, pm_node_t *target, c } /** - * Allocate a new LocalVariableReadNode node. + * Allocate a new LocalVariableReadNode node with constant_id. */ static pm_local_variable_read_node_t * -pm_local_variable_read_node_create(pm_parser_t *parser, const pm_token_t *name, uint32_t depth) { - pm_constant_id_t name_id = pm_parser_constant_id_token(parser, name); - +pm_local_variable_read_node_create_constant_id(pm_parser_t *parser, const pm_token_t *name, pm_constant_id_t name_id, uint32_t depth) { if (parser->current_param_name == name_id) { pm_parser_err_token(parser, name, PM_ERR_PARAMETER_CIRCULAR); } @@ -4224,6 +4222,15 @@ pm_local_variable_read_node_create(pm_parser_t *parser, const pm_token_t *name, return node; } +/** + * Allocate a new LocalVariableReadNode node. + */ +static pm_local_variable_read_node_t * +pm_local_variable_read_node_create(pm_parser_t *parser, const pm_token_t *name, uint32_t depth) { + pm_constant_id_t name_id = pm_parser_constant_id_token(parser, name); + return pm_local_variable_read_node_create_constant_id(parser, name, name_id, depth); +} + /** * Allocate and initialize a new LocalVariableWriteNode node. */ @@ -4249,6 +4256,57 @@ pm_local_variable_write_node_create(pm_parser_t *parser, pm_constant_id_t name, return node; } +/** + * Returns true if the given bounds comprise `it`. + */ +static inline bool +pm_token_is_it(const uint8_t *start, const uint8_t *end) { + return (end - start == 2) && (start[0] == 'i') && (start[1] == 't'); +} + +/** + * Returns true if the given node is `it` default parameter. + */ +static inline bool +pm_node_is_it(pm_parser_t *parser, pm_node_t *node) { + // Check if it's a local variable reference + if (node->type != PM_CALL_NODE) { + return false; + } + + // Check if it's a variable call + pm_call_node_t *call_node = (pm_call_node_t *) node; + if (!pm_call_node_variable_call_p(call_node)) { + return false; + } + + // Check if it's called `it` + pm_constant_id_t id = ((pm_call_node_t *)node)->name; + pm_constant_t *constant = pm_constant_pool_id_to_constant(&parser->constant_pool, id); + return pm_token_is_it(constant->start, constant->start + constant->length); +} + +/** + * Convert a `it` variable call node to a node for `it` default parameter. + */ +static pm_node_t * +pm_node_check_it(pm_parser_t *parser, pm_node_t *node) { + if ( + (parser->version != PM_OPTIONS_VERSION_CRUBY_3_3_0) && + !parser->current_scope->closed && + pm_node_is_it(parser, node) + ) { + if (parser->current_scope->explicit_params) { + pm_parser_err_previous(parser, PM_ERR_IT_NOT_ALLOWED); + } else { + pm_node_destroy(parser, node); + pm_constant_id_t name_id = pm_parser_constant_id_constant(parser, "0it", 3); + node = (pm_node_t *) pm_local_variable_read_node_create_constant_id(parser, &parser->previous, name_id, 0); + } + } + return node; +} + /** * Returns true if the given bounds comprise a numbered parameter (i.e., they * are of the form /^_\d$/). @@ -13549,8 +13607,13 @@ parse_pattern_primitive(pm_parser_t *parser, pm_diagnostic_id_t diag_id) { parser_lex(parser); pm_node_t *variable = (pm_node_t *) parse_variable(parser); if (variable == NULL) { - PM_PARSER_ERR_TOKEN_FORMAT(parser, parser->previous, PM_ERR_NO_LOCAL_VARIABLE, (int) (parser->previous.end - parser->previous.start), parser->previous.start); - variable = (pm_node_t *) pm_local_variable_read_node_create(parser, &parser->previous, 0); + if (parser->version != PM_OPTIONS_VERSION_CRUBY_3_3_0 && pm_token_is_it(parser->previous.start, parser->previous.end)) { + pm_constant_id_t name_id = pm_parser_constant_id_constant(parser, "0it", 3); + variable = (pm_node_t *) pm_local_variable_read_node_create_constant_id(parser, &parser->previous, name_id, 0); + } else { + PM_PARSER_ERR_TOKEN_FORMAT(parser, parser->previous, PM_ERR_NO_LOCAL_VARIABLE, (int) (parser->previous.end - parser->previous.start), parser->previous.start); + variable = (pm_node_t *) pm_local_variable_read_node_create(parser, &parser->previous, 0); + } } return (pm_node_t *) pm_pinned_variable_node_create(parser, &operator, variable); @@ -14452,6 +14515,31 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b if ((binding_power == PM_BINDING_POWER_STATEMENT) && match1(parser, PM_TOKEN_COMMA)) { node = parse_targets_validate(parser, node, PM_BINDING_POWER_INDEX); } + else { + // Check if `it` is not going to be assigned. + switch (parser->current.type) { + case PM_TOKEN_AMPERSAND_AMPERSAND_EQUAL: + case PM_TOKEN_AMPERSAND_EQUAL: + case PM_TOKEN_CARET_EQUAL: + case PM_TOKEN_EQUAL: + case PM_TOKEN_GREATER_GREATER_EQUAL: + case PM_TOKEN_LESS_LESS_EQUAL: + case PM_TOKEN_MINUS_EQUAL: + case PM_TOKEN_PARENTHESIS_RIGHT: + case PM_TOKEN_PERCENT_EQUAL: + case PM_TOKEN_PIPE_EQUAL: + case PM_TOKEN_PIPE_PIPE_EQUAL: + case PM_TOKEN_PLUS_EQUAL: + case PM_TOKEN_SLASH_EQUAL: + case PM_TOKEN_STAR_EQUAL: + case PM_TOKEN_STAR_STAR_EQUAL: + break; + default: + // Once we know it's neither a method call nor an assignment, + // we can finally create `it` default parameter. + node = pm_node_check_it(parser, node); + } + } return node; } @@ -15059,6 +15147,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b if (match2(parser, PM_TOKEN_DOT, PM_TOKEN_COLON_COLON)) { receiver = parse_variable_call(parser); + receiver = pm_node_check_it(parser, receiver); saved_param_name = pm_parser_current_param_name_unset(parser); pm_parser_scope_push(parser, true); diff --git a/test/prism/errors_test.rb b/test/prism/errors_test.rb index d1af9a5dae..60322585f0 100644 --- a/test/prism/errors_test.rb +++ b/test/prism/errors_test.rb @@ -2069,6 +2069,13 @@ def test_forwarding_arg_and_block ], compare_ripper: false # Ripper does not check 'both block arg and actual block given'. end + def test_it_with_ordinary_parameter + source = "proc { || it }" + errors = [["`it` is not allowed when an ordinary parameter is defined", 10..12]] + + assert_errors expression(source), source, errors, compare_ripper: false + end + private def assert_errors(expected, source, errors, compare_ripper: RUBY_ENGINE == "ruby") diff --git a/test/prism/location_test.rb b/test/prism/location_test.rb index e5b7546925..9d583635e0 100644 --- a/test/prism/location_test.rb +++ b/test/prism/location_test.rb @@ -174,6 +174,10 @@ def test_CallNode assert_location(CallNode, "foo bar baz") assert_location(CallNode, "foo bar('baz')") + + assert_location(CallNode, "-> { it }", 5...7, version: "3.3.0") do |node| + node.body.body.first + end end def test_CallAndWriteNode @@ -570,6 +574,12 @@ def test_LocalVariableOrWriteNode def test_LocalVariableReadNode assert_location(LocalVariableReadNode, "foo = 1; foo", 9...12) + assert_location(LocalVariableReadNode, "-> { it }", 5...7) do |node| + node.body.body.first + end + assert_location(LocalVariableReadNode, "foo { it }", 6...8) do |node| + node.block.body.body.first + end end def test_LocalVariableTargetNode @@ -674,6 +684,9 @@ def test_PinnedExpressionNode def test_PinnedVariableNode assert_location(PinnedVariableNode, "bar = 1; foo in ^bar", 16...20, &:pattern) + assert_location(PinnedVariableNode, "proc { 1 in ^it }.call(1)", 12...15) do |node| + node.receiver.block.body.body.first.pattern + end end def test_PostExecutionNode @@ -891,8 +904,8 @@ def test_all_tested private - def assert_location(kind, source, expected = 0...source.length) - result = Prism.parse(source) + def assert_location(kind, source, expected = 0...source.length, **options) + result = Prism.parse(source, **options) assert_equal [], result.comments assert_equal [], result.errors