Skip to content

Commit

Permalink
Track duplicate hash keys for pattern matching
Browse files Browse the repository at this point in the history
  • Loading branch information
kddnewton committed Apr 1, 2024
1 parent aa2182f commit 71ea82f
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 2 deletions.
1 change: 1 addition & 0 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ errors:
- PATTERN_EXPRESSION_AFTER_RANGE
- PATTERN_EXPRESSION_AFTER_REST
- PATTERN_HASH_KEY
- PATTERN_HASH_KEY_DUPLICATE
- PATTERN_HASH_KEY_LABEL
- PATTERN_IDENT_AFTER_HROCKET
- PATTERN_LABEL_AFTER_COMMA
Expand Down
12 changes: 12 additions & 0 deletions src/prism.c
Original file line number Diff line number Diff line change
Expand Up @@ -14828,12 +14828,20 @@ parse_pattern_hash_implicit_value(pm_parser_t *parser, pm_constant_id_list_t *ca
return (pm_node_t *) pm_implicit_node_create(parser, (pm_node_t *) target);
}

static void
parse_pattern_hash_key(pm_parser_t *parser, pm_static_literals_t *keys, pm_node_t *node) {
if (pm_static_literals_add(parser, keys, node) != NULL) {
pm_parser_err_node(parser, node, PM_ERR_PATTERN_HASH_KEY_DUPLICATE);
}
}

/**
* Parse a hash pattern.
*/
static pm_hash_pattern_node_t *
parse_pattern_hash(pm_parser_t *parser, pm_constant_id_list_t *captures, pm_node_t *first_node) {
pm_node_list_t assocs = { 0 };
pm_static_literals_t keys = { 0 };
pm_node_t *rest = NULL;

switch (PM_NODE_TYPE(first_node)) {
Expand All @@ -14843,6 +14851,7 @@ parse_pattern_hash(pm_parser_t *parser, pm_constant_id_list_t *captures, pm_node
break;
case PM_SYMBOL_NODE: {
if (pm_symbol_node_label_p(first_node)) {
parse_pattern_hash_key(parser, &keys, first_node);
pm_node_t *value;

if (match7(parser, PM_TOKEN_COMMA, PM_TOKEN_KEYWORD_THEN, PM_TOKEN_BRACE_RIGHT, PM_TOKEN_BRACKET_RIGHT, PM_TOKEN_PARENTHESIS_RIGHT, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON)) {
Expand Down Expand Up @@ -14897,6 +14906,8 @@ parse_pattern_hash(pm_parser_t *parser, pm_constant_id_list_t *captures, pm_node
} else {
expect1(parser, PM_TOKEN_LABEL, PM_ERR_PATTERN_LABEL_AFTER_COMMA);
pm_node_t *key = (pm_node_t *) pm_symbol_node_label_create(parser, &parser->previous);

parse_pattern_hash_key(parser, &keys, key);
pm_node_t *value = NULL;

if (match7(parser, PM_TOKEN_COMMA, PM_TOKEN_KEYWORD_THEN, PM_TOKEN_BRACE_RIGHT, PM_TOKEN_BRACKET_RIGHT, PM_TOKEN_PARENTHESIS_RIGHT, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON)) {
Expand All @@ -14919,6 +14930,7 @@ parse_pattern_hash(pm_parser_t *parser, pm_constant_id_list_t *captures, pm_node
pm_hash_pattern_node_t *node = pm_hash_pattern_node_node_list_create(parser, &assocs, rest);
xfree(assocs.nodes);

pm_static_literals_free(&keys);
return node;
}

Expand Down
1 change: 1 addition & 0 deletions templates/src/diagnostic.c.erb
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = {
[PM_ERR_PATTERN_EXPRESSION_AFTER_RANGE] = { "expected a pattern expression after the range operator", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_PATTERN_EXPRESSION_AFTER_REST] = { "unexpected pattern expression after the `**` expression", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_PATTERN_HASH_KEY] = { "expected a key in the hash pattern", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_PATTERN_HASH_KEY_DUPLICATE] = { "duplicated key name", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_PATTERN_HASH_KEY_LABEL] = { "expected a label as the key in the hash pattern", PM_ERROR_LEVEL_SYNTAX }, // TODO // THIS // AND // ABOVE // IS WEIRD
[PM_ERR_PATTERN_IDENT_AFTER_HROCKET] = { "expected an identifier after the `=>` operator", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_PATTERN_LABEL_AFTER_COMMA] = { "expected a label after the `,` in the hash pattern", PM_ERROR_LEVEL_SYNTAX },
Expand Down
14 changes: 12 additions & 2 deletions test/prism/errors_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2161,8 +2161,7 @@ def test_duplicate_pattern_capture
source = <<~RUBY
() => [a, a]
() => [a, *a]
() => {a:, a:}
() => {a: a, a: a}
() => {a: a, b: a}
() => {a: a, **a}
() => [a, {a:}]
() => [a, {a: {a: {a: [a]}}}]
Expand All @@ -2173,6 +2172,12 @@ def test_duplicate_pattern_capture
assert_error_messages source, Array.new(source.lines.length, "duplicated variable name"), compare_ripper: false
end

def test_duplicate_pattern_hash_key
assert_error_messages "() => {a:, a:}", ["duplicated key name", "duplicated variable name"]
assert_error_messages "() => {a:1, a:2}", ["duplicated key name"]
refute_error_messages "() => [{a:1}, {a:2}]"
end

private

def assert_errors(expected, source, errors, compare_ripper: RUBY_ENGINE == "ruby")
Expand All @@ -2192,6 +2197,11 @@ def assert_error_messages(source, errors, compare_ripper: RUBY_ENGINE == "ruby")
assert_equal(errors, result.errors.map(&:message))
end

def refute_error_messages(source, compare_ripper: RUBY_ENGINE == "ruby")
refute_nil Ripper.sexp_raw(source) if compare_ripper
assert Prism.parse_success?(source)
end

def assert_warning_messages(source, warnings)
result = Prism.parse(source)
assert_equal(warnings, result.warnings.map(&:message))
Expand Down

0 comments on commit 71ea82f

Please sign in to comment.