diff options
-rw-r--r-- | prism/config.yml | 3 | ||||
-rw-r--r-- | prism/parser.h | 6 | ||||
-rw-r--r-- | prism/prism.c | 411 | ||||
-rw-r--r-- | prism/templates/src/diagnostic.c.erb | 3 | ||||
-rw-r--r-- | test/prism/result/warnings_test.rb | 73 |
5 files changed, 391 insertions, 105 deletions
diff --git a/prism/config.yml b/prism/config.yml index ad5d633755..fed8265173 100644 --- a/prism/config.yml +++ b/prism/config.yml @@ -298,10 +298,11 @@ warnings: - DUPLICATED_WHEN_CLAUSE - FLOAT_OUT_OF_RANGE - IGNORED_FROZEN_STRING_LITERAL + - INDENTATION_MISMATCH - INTEGER_IN_FLIP_FLOP - INVALID_CHARACTER + - INVALID_MAGIC_COMMENT_VALUE - INVALID_NUMBERED_REFERENCE - - INVALID_SHAREABLE_CONSTANT_VALUE - KEYWORD_EOL - LITERAL_IN_CONDITION_DEFAULT - LITERAL_IN_CONDITION_VERBOSE diff --git a/prism/parser.h b/prism/parser.h index 847b29f368..ea40fc910a 100644 --- a/prism/parser.h +++ b/prism/parser.h @@ -902,6 +902,12 @@ struct pm_parser { * characters. */ bool current_regular_expression_ascii_only; + + /** + * By default, Ruby always warns about mismatched indentation. This can be + * toggled with a magic comment. + */ + bool warn_mismatched_indentation; }; #endif diff --git a/prism/prism.c b/prism/prism.c index 02ccecfc12..1f63b89e54 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -8266,16 +8266,24 @@ parser_lex_magic_comment_encoding(pm_parser_t *parser) { } } +typedef enum { + PM_MAGIC_COMMENT_BOOLEAN_VALUE_TRUE, + PM_MAGIC_COMMENT_BOOLEAN_VALUE_FALSE, + PM_MAGIC_COMMENT_BOOLEAN_VALUE_INVALID +} pm_magic_comment_boolean_value_t; + /** * Check if this is a magic comment that includes the frozen_string_literal * pragma. If it does, set that field on the parser. */ -static void -parser_lex_magic_comment_frozen_string_literal_value(pm_parser_t *parser, const uint8_t *start, const uint8_t *end) { - if ((start + 4 <= end) && pm_strncasecmp(start, (const uint8_t *) "true", 4) == 0) { - parser->frozen_string_literal = PM_OPTIONS_FROZEN_STRING_LITERAL_ENABLED; - } else if ((start + 5 <= end) && pm_strncasecmp(start, (const uint8_t *) "false", 5) == 0) { - parser->frozen_string_literal = PM_OPTIONS_FROZEN_STRING_LITERAL_DISABLED; +static pm_magic_comment_boolean_value_t +parser_lex_magic_comment_boolean_value(const uint8_t *value_start, uint32_t value_length) { + if (value_length == 4 && pm_strncasecmp(value_start, (const uint8_t *) "true", 4) == 0) { + return PM_MAGIC_COMMENT_BOOLEAN_VALUE_TRUE; + } else if (value_length == 5 && pm_strncasecmp(value_start, (const uint8_t *) "false", 5) == 0) { + return PM_MAGIC_COMMENT_BOOLEAN_VALUE_FALSE; + } else { + return PM_MAGIC_COMMENT_BOOLEAN_VALUE_INVALID; } } @@ -8364,6 +8372,7 @@ parser_lex_magic_comment(pm_parser_t *parser, bool semantic_token_seen) { if (*cursor == '\\' && (cursor + 1 < end)) cursor++; } value_end = cursor; + if (*cursor == '"') cursor++; } else { value_start = cursor; while (cursor < end && *cursor != '"' && *cursor != ';' && !pm_char_is_whitespace(*cursor)) cursor++; @@ -8381,28 +8390,28 @@ parser_lex_magic_comment(pm_parser_t *parser, bool semantic_token_seen) { // underscores. We only need to do this if there _is_ a dash in the key. pm_string_t key; const size_t key_length = (size_t) (key_end - key_start); - const uint8_t *dash = pm_memchr(key_start, '-', (size_t) key_length, parser->encoding_changed, parser->encoding); + const uint8_t *dash = pm_memchr(key_start, '-', key_length, parser->encoding_changed, parser->encoding); if (dash == NULL) { pm_string_shared_init(&key, key_start, key_end); } else { - size_t width = (size_t) (key_end - key_start); - uint8_t *buffer = xmalloc(width); + uint8_t *buffer = xmalloc(key_length); if (buffer == NULL) break; - memcpy(buffer, key_start, width); + memcpy(buffer, key_start, key_length); buffer[dash - key_start] = '_'; while ((dash = pm_memchr(dash + 1, '-', (size_t) (key_end - dash - 1), parser->encoding_changed, parser->encoding)) != NULL) { buffer[dash - key_start] = '_'; } - pm_string_owned_init(&key, buffer, width); + pm_string_owned_init(&key, buffer, key_length); } // Finally, we can start checking the key against the list of known // magic comment keys, and potentially change state based on that. const uint8_t *key_source = pm_string_source(&key); + uint32_t value_length = (uint32_t) (value_end - value_start); // We only want to attempt to compare against encoding comments if it's // the first line in the file (or the second in the case of a shebang). @@ -8415,40 +8424,82 @@ parser_lex_magic_comment(pm_parser_t *parser, bool semantic_token_seen) { } } - // We only want to handle frozen string literal comments if it's before - // any semantic tokens have been seen. - if (key_length == 21 && pm_strncasecmp(key_source, (const uint8_t *) "frozen_string_literal", 21) == 0) { - if (semantic_token_seen) { - pm_parser_warn_token(parser, &parser->current, PM_WARN_IGNORED_FROZEN_STRING_LITERAL); - } else { - parser_lex_magic_comment_frozen_string_literal_value(parser, value_start, value_end); + if (key_length == 11) { + if (pm_strncasecmp(key_source, (const uint8_t *) "warn_indent", 11) == 0) { + switch (parser_lex_magic_comment_boolean_value(value_start, value_length)) { + case PM_MAGIC_COMMENT_BOOLEAN_VALUE_INVALID: + PM_PARSER_WARN_TOKEN_FORMAT( + parser, + parser->current, + PM_WARN_INVALID_MAGIC_COMMENT_VALUE, + (int) key_length, + (const char *) key_source, + (int) value_length, + (const char *) value_start + ); + break; + case PM_MAGIC_COMMENT_BOOLEAN_VALUE_FALSE: + parser->warn_mismatched_indentation = false; + break; + case PM_MAGIC_COMMENT_BOOLEAN_VALUE_TRUE: + parser->warn_mismatched_indentation = true; + break; + } } - } - - // If we have hit a ractor pragma, attempt to lex that. - uint32_t value_length = (uint32_t) (value_end - value_start); - if (key_length == 24 && pm_strncasecmp(key_source, (const uint8_t *) "shareable_constant_value", 24) == 0) { - const uint8_t *cursor = parser->current.start; - while ((cursor > parser->start) && ((cursor[-1] == ' ') || (cursor[-1] == '\t'))) cursor--; - - if (!((cursor == parser->start) || (cursor[-1] == '\n'))) { - pm_parser_warn_token(parser, &parser->current, PM_WARN_SHAREABLE_CONSTANT_VALUE_LINE); - } else if (value_length == 4 && pm_strncasecmp(value_start, (const uint8_t *) "none", 4) == 0) { - pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_NONE); - } else if (value_length == 7 && pm_strncasecmp(value_start, (const uint8_t *) "literal", 7) == 0) { - pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_LITERAL); - } else if (value_length == 23 && pm_strncasecmp(value_start, (const uint8_t *) "experimental_everything", 23) == 0) { - pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_EXPERIMENTAL_EVERYTHING); - } else if (value_length == 17 && pm_strncasecmp(value_start, (const uint8_t *) "experimental_copy", 17) == 0) { - pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_EXPERIMENTAL_COPY); - } else { - PM_PARSER_WARN_TOKEN_FORMAT( - parser, - parser->current, - PM_WARN_INVALID_SHAREABLE_CONSTANT_VALUE, - (int) value_length, - (const char *) value_start - ); + } else if (key_length == 21) { + if (pm_strncasecmp(key_source, (const uint8_t *) "frozen_string_literal", 21) == 0) { + // We only want to handle frozen string literal comments if it's + // before any semantic tokens have been seen. + if (semantic_token_seen) { + pm_parser_warn_token(parser, &parser->current, PM_WARN_IGNORED_FROZEN_STRING_LITERAL); + } else { + switch (parser_lex_magic_comment_boolean_value(value_start, value_length)) { + case PM_MAGIC_COMMENT_BOOLEAN_VALUE_INVALID: + PM_PARSER_WARN_TOKEN_FORMAT( + parser, + parser->current, + PM_WARN_INVALID_MAGIC_COMMENT_VALUE, + (int) key_length, + (const char *) key_source, + (int) value_length, + (const char *) value_start + ); + break; + case PM_MAGIC_COMMENT_BOOLEAN_VALUE_FALSE: + parser->frozen_string_literal = PM_OPTIONS_FROZEN_STRING_LITERAL_DISABLED; + break; + case PM_MAGIC_COMMENT_BOOLEAN_VALUE_TRUE: + parser->frozen_string_literal = PM_OPTIONS_FROZEN_STRING_LITERAL_ENABLED; + break; + } + } + } + } else if (key_length == 24) { + if (pm_strncasecmp(key_source, (const uint8_t *) "shareable_constant_value", 24) == 0) { + const uint8_t *cursor = parser->current.start; + while ((cursor > parser->start) && ((cursor[-1] == ' ') || (cursor[-1] == '\t'))) cursor--; + + if (!((cursor == parser->start) || (cursor[-1] == '\n'))) { + pm_parser_warn_token(parser, &parser->current, PM_WARN_SHAREABLE_CONSTANT_VALUE_LINE); + } else if (value_length == 4 && pm_strncasecmp(value_start, (const uint8_t *) "none", 4) == 0) { + pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_NONE); + } else if (value_length == 7 && pm_strncasecmp(value_start, (const uint8_t *) "literal", 7) == 0) { + pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_LITERAL); + } else if (value_length == 23 && pm_strncasecmp(value_start, (const uint8_t *) "experimental_everything", 23) == 0) { + pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_EXPERIMENTAL_EVERYTHING); + } else if (value_length == 17 && pm_strncasecmp(value_start, (const uint8_t *) "experimental_copy", 17) == 0) { + pm_parser_scope_shareable_constant_set(parser, PM_SCOPE_SHAREABLE_CONSTANT_EXPERIMENTAL_COPY); + } else { + PM_PARSER_WARN_TOKEN_FORMAT( + parser, + parser->current, + PM_WARN_INVALID_MAGIC_COMMENT_VALUE, + (int) key_length, + (const char *) key_source, + (int) value_length, + (const char *) value_start + ); + } } } @@ -8462,7 +8513,7 @@ parser_lex_magic_comment(pm_parser_t *parser, bool semantic_token_seen) { magic_comment->key_start = key_start; magic_comment->value_start = value_start; magic_comment->key_length = (uint32_t) key_length; - magic_comment->value_length = (uint32_t) (value_end - value_start); + magic_comment->value_length = value_length; pm_list_append(&parser->magic_comment_list, (pm_list_node_t *) magic_comment); } } @@ -14653,6 +14704,103 @@ parse_parameters( return params; } +/** + * Accepts a parser returns the index of the last newline in the file that was + * ecorded before the current token within the newline list. + */ +static size_t +token_newline_index(const pm_parser_t *parser) { + if (parser->heredoc_end == NULL) { + // This is the common case. In this case we can look at the previously + // recorded newline in the newline list and subtract from the current + // offset. + return parser->newline_list.size - 1; + } else { + // This is unlikely. This is the case that we have already parsed the + // start of a heredoc, so we cannot rely on looking at the previous + // offset of the newline list, and instead must go through the whole + // process of a binary search for the line number. + return (size_t) pm_newline_list_line(&parser->newline_list, parser->current.start, 0); + } +} + +/** + * Accepts a parser, a newline index, and a token and returns the column. The + * important piece of this is that it expands tabs out to the next tab stop. + */ +static int64_t +token_column(const pm_parser_t *parser, size_t newline_index, const pm_token_t *token, bool break_on_non_space) { + const uint8_t *cursor = parser->start + parser->newline_list.offsets[newline_index]; + const uint8_t *end = token->start; + + // Skip over the BOM if it is present. + if ( + newline_index == 0 && + parser->start[0] == 0xef && + parser->start[1] == 0xbb && + parser->start[2] == 0xbf + ) cursor += 3; + + int64_t column = 0; + for (; cursor < end; cursor++) { + switch (*cursor) { + case '\t': + column = ((column / PM_TAB_WHITESPACE_SIZE) + 1) * PM_TAB_WHITESPACE_SIZE; + break; + case ' ': + column++; + break; + default: + column++; + if (break_on_non_space) return -1; + break; + } + } + + return column; +} + +/** + * Accepts a parser, two newline indices, and pointers to two tokens. This + * function warns if the indentation of the two tokens does not match. + */ +static void +parser_warn_indentation_mismatch(pm_parser_t *parser, size_t opening_newline_index, const pm_token_t *opening_token, bool if_after_else) { + // If these warnings are disabled (unlikely), then we can just return. + if (!parser->warn_mismatched_indentation) return; + + // If the tokens are on the same line, we do not warn. + size_t closing_newline_index = token_newline_index(parser); + if (opening_newline_index == closing_newline_index) return; + + // If the opening token has anything other than spaces or tabs before it, + // then we do not warn. This is unless we are matching up an `if`/`end` pair + // and the `if` immediately follows an `else` keyword. + int64_t opening_column = token_column(parser, opening_newline_index, opening_token, !if_after_else); + if (!if_after_else && (opening_column == -1)) return; + + // Get a reference to the closing token off the current parser. This assumes + // that the caller has placed this in the correct position. + pm_token_t *closing_token = &parser->current; + + // If the tokens are at the same indentation, we do not warn. + int64_t closing_column = token_column(parser, closing_newline_index, closing_token, true); + if ((closing_column == -1) || (opening_column == closing_column)) return; + + // Otherwise, add a warning. + PM_PARSER_WARN_FORMAT( + parser, + closing_token->start, + closing_token->end, + PM_WARN_INDENTATION_MISMATCH, + (int) (closing_token->end - closing_token->start), + (const char *) closing_token->start, + (int) (opening_token->end - opening_token->start), + (const char *) opening_token->start, + ((int32_t) opening_newline_index) + parser->start_line + ); +} + typedef enum { PM_RESCUES_BEGIN = 1, PM_RESCUES_BLOCK, @@ -14668,10 +14816,13 @@ typedef enum { * nodes pointing to each other from the top. */ static inline void -parse_rescues(pm_parser_t *parser, pm_begin_node_t *parent_node, pm_rescues_type_t type) { +parse_rescues(pm_parser_t *parser, size_t opening_newline_index, const pm_token_t *opening, pm_begin_node_t *parent_node, pm_rescues_type_t type) { pm_rescue_node_t *current = NULL; - while (accept1(parser, PM_TOKEN_KEYWORD_RESCUE)) { + while (match1(parser, PM_TOKEN_KEYWORD_RESCUE)) { + if (opening != NULL) parser_warn_indentation_mismatch(parser, opening_newline_index, opening, false); + parser_lex(parser); + pm_rescue_node_t *rescue = pm_rescue_node_create(parser, &parser->previous); switch (parser->current.type) { @@ -14763,17 +14914,25 @@ parse_rescues(pm_parser_t *parser, pm_begin_node_t *parent_node, pm_rescues_type // The end node locations on rescue nodes will not be set correctly // since we won't know the end until we've found all consequent // clauses. This sets the end location on all rescues once we know it. - if (current) { + if (current != NULL) { const uint8_t *end_to_set = current->base.location.end; - current = parent_node->rescue_clause; - while (current) { - current->base.location.end = end_to_set; - current = current->consequent; + pm_rescue_node_t *clause = parent_node->rescue_clause; + + while (clause != NULL) { + clause->base.location.end = end_to_set; + clause = clause->consequent; } } - if (accept1(parser, PM_TOKEN_KEYWORD_ELSE)) { - pm_token_t else_keyword = parser->previous; + pm_token_t else_keyword; + if (match1(parser, PM_TOKEN_KEYWORD_ELSE)) { + if (opening != NULL) parser_warn_indentation_mismatch(parser, opening_newline_index, opening, false); + opening_newline_index = token_newline_index(parser); + + else_keyword = parser->current; + opening = &else_keyword; + + parser_lex(parser); accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON); pm_statements_node_t *else_statements = NULL; @@ -14800,10 +14959,17 @@ parse_rescues(pm_parser_t *parser, pm_begin_node_t *parent_node, pm_rescues_type pm_else_node_t *else_clause = pm_else_node_create(parser, &else_keyword, else_statements, &parser->current); pm_begin_node_else_clause_set(parent_node, else_clause); + + // If we don't have a `current` rescue node, then this is a dangling + // else, and it's an error. + if (current == NULL) pm_parser_err_node(parser, (pm_node_t *) else_clause, PM_ERR_BEGIN_LONELY_ELSE); } - if (accept1(parser, PM_TOKEN_KEYWORD_ENSURE)) { - pm_token_t ensure_keyword = parser->previous; + if (match1(parser, PM_TOKEN_KEYWORD_ENSURE)) { + if (opening != NULL) parser_warn_indentation_mismatch(parser, opening_newline_index, opening, false); + pm_token_t ensure_keyword = parser->current; + + parser_lex(parser); accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON); pm_statements_node_t *ensure_statements = NULL; @@ -14832,7 +14998,8 @@ parse_rescues(pm_parser_t *parser, pm_begin_node_t *parent_node, pm_rescues_type pm_begin_node_ensure_clause_set(parent_node, ensure_clause); } - if (parser->current.type == PM_TOKEN_KEYWORD_END) { + if (match1(parser, PM_TOKEN_KEYWORD_END)) { + if (opening != NULL) parser_warn_indentation_mismatch(parser, opening_newline_index, opening, false); pm_begin_node_end_keyword_set(parent_node, &parser->current); } else { pm_token_t end_keyword = (pm_token_t) { .type = PM_TOKEN_MISSING, .start = parser->previous.end, .end = parser->previous.end }; @@ -14845,11 +15012,11 @@ parse_rescues(pm_parser_t *parser, pm_begin_node_t *parent_node, pm_rescues_type * class, module, def, etc.). */ static pm_begin_node_t * -parse_rescues_implicit_begin(pm_parser_t *parser, const uint8_t *start, pm_statements_node_t *statements, pm_rescues_type_t type) { +parse_rescues_implicit_begin(pm_parser_t *parser, size_t opening_newline_index, const pm_token_t *opening, const uint8_t *start, pm_statements_node_t *statements, pm_rescues_type_t type) { pm_token_t begin_keyword = not_provided(parser); pm_begin_node_t *node = pm_begin_node_create(parser, &begin_keyword, statements); - parse_rescues(parser, node, type); + parse_rescues(parser, opening_newline_index, opening, node, type); node->base.location.start = start; return node; @@ -15070,7 +15237,7 @@ parse_block(pm_parser_t *parser) { if (match2(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE)) { assert(statements == NULL || PM_NODE_TYPE_P(statements, PM_STATEMENTS_NODE)); - statements = (pm_node_t *) parse_rescues_implicit_begin(parser, opening.start, (pm_statements_node_t *) statements, PM_RESCUES_BLOCK); + statements = (pm_node_t *) parse_rescues_implicit_begin(parser, 0, NULL, opening.start, (pm_statements_node_t *) statements, PM_RESCUES_BLOCK); } } @@ -15340,7 +15507,7 @@ parse_predicate(pm_parser_t *parser, pm_binding_power_t binding_power, pm_contex } static inline pm_node_t * -parse_conditional(pm_parser_t *parser, pm_context_t context) { +parse_conditional(pm_parser_t *parser, pm_context_t context, size_t opening_newline_index, bool if_after_else) { pm_node_list_t current_block_exits = { 0 }; pm_node_list_t *previous_block_exits = push_block_exits(parser, ¤t_block_exits); @@ -15382,6 +15549,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context) { PM_PARSER_WARN_TOKEN_FORMAT_CONTENT(parser, parser->current, PM_WARN_KEYWORD_EOL); } + parser_warn_indentation_mismatch(parser, opening_newline_index, &keyword, false); pm_token_t elsif_keyword = parser->current; parser_lex(parser); @@ -15399,6 +15567,9 @@ parse_conditional(pm_parser_t *parser, pm_context_t context) { } if (match1(parser, PM_TOKEN_KEYWORD_ELSE)) { + parser_warn_indentation_mismatch(parser, opening_newline_index, &keyword, false); + opening_newline_index = token_newline_index(parser); + parser_lex(parser); pm_token_t else_keyword = parser->previous; @@ -15407,6 +15578,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context) { pm_accepts_block_stack_pop(parser); accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON); + parser_warn_indentation_mismatch(parser, opening_newline_index, &else_keyword, false); expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_CONDITIONAL_TERM_ELSE); pm_else_node_t *else_node = pm_else_node_create(parser, &else_keyword, else_statements, &parser->previous); @@ -15423,8 +15595,7 @@ parse_conditional(pm_parser_t *parser, pm_context_t context) { break; } } else { - // We should specialize this error message to refer to 'if' or 'unless' - // explicitly. + parser_warn_indentation_mismatch(parser, opening_newline_index, &keyword, if_after_else); expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_CONDITIONAL_TERM); } @@ -18190,7 +18361,9 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b } } case PM_TOKEN_KEYWORD_CASE: { + size_t opening_newline_index = token_newline_index(parser); parser_lex(parser); + pm_token_t case_keyword = parser->previous; pm_node_t *predicate = NULL; @@ -18209,7 +18382,10 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b while (accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON)); } - if (accept1(parser, PM_TOKEN_KEYWORD_END)) { + if (match1(parser, PM_TOKEN_KEYWORD_END)) { + parser_warn_indentation_mismatch(parser, opening_newline_index, &case_keyword, false); + parser_lex(parser); + pop_block_exits(parser, previous_block_exits); pm_node_list_free(¤t_block_exits); @@ -18229,7 +18405,10 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b // At this point we've seen a when keyword, so we know this is a // case-when node. We will continue to parse the when nodes // until we hit the end of the list. - while (accept1(parser, PM_TOKEN_KEYWORD_WHEN)) { + while (match1(parser, PM_TOKEN_KEYWORD_WHEN)) { + parser_warn_indentation_mismatch(parser, opening_newline_index, &case_keyword, false); + parser_lex(parser); + pm_token_t when_keyword = parser->previous; pm_when_node_t *when_node = pm_when_node_create(parser, &when_keyword); @@ -18302,6 +18481,8 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b // will continue to parse the in nodes until we hit the end of // the list. while (match1(parser, PM_TOKEN_KEYWORD_IN)) { + parser_warn_indentation_mismatch(parser, opening_newline_index, &case_keyword, false); + bool previous_pattern_matching_newlines = parser->pattern_matching_newlines; parser->pattern_matching_newlines = true; @@ -18387,7 +18568,9 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b } } + parser_warn_indentation_mismatch(parser, opening_newline_index, &case_keyword, false); expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_CASE_TERM); + if (PM_NODE_TYPE_P(node, PM_CASE_NODE)) { pm_case_node_end_keyword_loc_set((pm_case_node_t *) node, &parser->previous); } else { @@ -18400,6 +18583,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b return node; } case PM_TOKEN_KEYWORD_BEGIN: { + size_t opening_newline_index = token_newline_index(parser); parser_lex(parser); pm_token_t begin_keyword = parser->previous; @@ -18409,7 +18593,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b pm_node_list_t *previous_block_exits = push_block_exits(parser, ¤t_block_exits); pm_statements_node_t *begin_statements = NULL; - if (!match3(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_END)) { + if (!match4(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_ELSE, PM_TOKEN_KEYWORD_END)) { pm_accepts_block_stack_push(parser, true); begin_statements = parse_statements(parser, PM_CONTEXT_BEGIN); pm_accepts_block_stack_pop(parser); @@ -18417,16 +18601,12 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b } pm_begin_node_t *begin_node = pm_begin_node_create(parser, &begin_keyword, begin_statements); - parse_rescues(parser, begin_node, PM_RESCUES_BEGIN); - + parse_rescues(parser, opening_newline_index, &begin_keyword, begin_node, PM_RESCUES_BEGIN); expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_BEGIN_TERM); + begin_node->base.location.end = parser->previous.end; pm_begin_node_end_keyword_set(begin_node, &parser->previous); - if ((begin_node->else_clause != NULL) && (begin_node->rescue_clause == NULL)) { - pm_parser_err_node(parser, (pm_node_t *) begin_node->else_clause, PM_ERR_BEGIN_LONELY_ELSE); - } - pop_block_exits(parser, previous_block_exits); pm_node_list_free(¤t_block_exits); @@ -18542,7 +18722,9 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b return node; } case PM_TOKEN_KEYWORD_CLASS: { + size_t opening_newline_index = token_newline_index(parser); parser_lex(parser); + pm_token_t class_keyword = parser->previous; pm_do_loop_stack_push(parser, false); @@ -18557,7 +18739,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON); pm_node_t *statements = NULL; - if (!match3(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_END)) { + if (!match4(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_ELSE, PM_TOKEN_KEYWORD_END)) { pm_accepts_block_stack_push(parser, true); statements = (pm_node_t *) parse_statements(parser, PM_CONTEXT_SCLASS); pm_accepts_block_stack_pop(parser); @@ -18565,7 +18747,9 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b if (match2(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE)) { assert(statements == NULL || PM_NODE_TYPE_P(statements, PM_STATEMENTS_NODE)); - statements = (pm_node_t *) parse_rescues_implicit_begin(parser, class_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_SCLASS); + statements = (pm_node_t *) parse_rescues_implicit_begin(parser, opening_newline_index, &class_keyword, class_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_SCLASS); + } else { + parser_warn_indentation_mismatch(parser, opening_newline_index, &class_keyword, false); } expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_CLASS_TERM); @@ -18613,7 +18797,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b } pm_node_t *statements = NULL; - if (!match3(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_END)) { + if (!match4(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_ELSE, PM_TOKEN_KEYWORD_END)) { pm_accepts_block_stack_push(parser, true); statements = (pm_node_t *) parse_statements(parser, PM_CONTEXT_CLASS); pm_accepts_block_stack_pop(parser); @@ -18621,7 +18805,9 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b if (match2(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE)) { assert(statements == NULL || PM_NODE_TYPE_P(statements, PM_STATEMENTS_NODE)); - statements = (pm_node_t *) parse_rescues_implicit_begin(parser, class_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_CLASS); + statements = (pm_node_t *) parse_rescues_implicit_begin(parser, opening_newline_index, &class_keyword, class_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_CLASS); + } else { + parser_warn_indentation_mismatch(parser, opening_newline_index, &class_keyword, false); } expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_CLASS_TERM); @@ -18650,6 +18836,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b pm_node_list_t *previous_block_exits = push_block_exits(parser, ¤t_block_exits); pm_token_t def_keyword = parser->current; + size_t opening_newline_index = token_newline_index(parser); pm_node_t *receiver = NULL; pm_token_t operator = not_provided(parser); @@ -18881,19 +19068,22 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b pm_accepts_block_stack_push(parser, true); pm_do_loop_stack_push(parser, false); - if (!match3(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_END)) { + if (!match4(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_ELSE, PM_TOKEN_KEYWORD_END)) { pm_accepts_block_stack_push(parser, true); statements = (pm_node_t *) parse_statements(parser, PM_CONTEXT_DEF); pm_accepts_block_stack_pop(parser); } - if (match2(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE)) { + if (match3(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_ELSE)) { assert(statements == NULL || PM_NODE_TYPE_P(statements, PM_STATEMENTS_NODE)); - statements = (pm_node_t *) parse_rescues_implicit_begin(parser, def_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_DEF); + statements = (pm_node_t *) parse_rescues_implicit_begin(parser, opening_newline_index, &def_keyword, def_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_DEF); + } else { + parser_warn_indentation_mismatch(parser, opening_newline_index, &def_keyword, false); } pm_accepts_block_stack_pop(parser); pm_do_loop_stack_pop(parser); + expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_DEF_TERM); end_keyword = parser->previous; } @@ -18984,9 +19174,11 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b } case PM_TOKEN_KEYWORD_FALSE: parser_lex(parser); - return (pm_node_t *)pm_false_node_create(parser, &parser->previous); + return (pm_node_t *) pm_false_node_create(parser, &parser->previous); case PM_TOKEN_KEYWORD_FOR: { + size_t opening_newline_index = token_newline_index(parser); parser_lex(parser); + pm_token_t for_keyword = parser->previous; pm_node_t *index; @@ -19033,13 +19225,15 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b } accept2(parser, PM_TOKEN_SEMICOLON, PM_TOKEN_NEWLINE); - pm_statements_node_t *statements = NULL; - if (!accept1(parser, PM_TOKEN_KEYWORD_END)) { + pm_statements_node_t *statements = NULL; + if (!match1(parser, PM_TOKEN_KEYWORD_END)) { statements = parse_statements(parser, PM_CONTEXT_FOR); - expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_FOR_TERM); } + parser_warn_indentation_mismatch(parser, opening_newline_index, &for_keyword, false); + expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_FOR_TERM); + return (pm_node_t *) pm_for_node_create(parser, index, collection, statements, &for_keyword, &in_keyword, &do_keyword, &parser->previous); } case PM_TOKEN_KEYWORD_IF: @@ -19047,8 +19241,11 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b PM_PARSER_WARN_TOKEN_FORMAT_CONTENT(parser, parser->current, PM_WARN_KEYWORD_EOL); } + size_t opening_newline_index = token_newline_index(parser); + bool if_after_else = parser->previous.type == PM_TOKEN_KEYWORD_ELSE; parser_lex(parser); - return parse_conditional(parser, PM_CONTEXT_IF); + + return parse_conditional(parser, PM_CONTEXT_IF, opening_newline_index, if_after_else); case PM_TOKEN_KEYWORD_UNDEF: { if (binding_power != PM_BINDING_POWER_STATEMENT) { pm_parser_err_current(parser, PM_ERR_STATEMENT_UNDEF); @@ -19108,13 +19305,17 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b return (pm_node_t *) pm_call_node_not_create(parser, receiver, &message, &arguments); } - case PM_TOKEN_KEYWORD_UNLESS: + case PM_TOKEN_KEYWORD_UNLESS: { + size_t opening_newline_index = token_newline_index(parser); parser_lex(parser); - return parse_conditional(parser, PM_CONTEXT_UNLESS); + + return parse_conditional(parser, PM_CONTEXT_UNLESS, opening_newline_index, false); + } case PM_TOKEN_KEYWORD_MODULE: { pm_node_list_t current_block_exits = { 0 }; pm_node_list_t *previous_block_exits = push_block_exits(parser, ¤t_block_exits); + size_t opening_newline_index = token_newline_index(parser); parser_lex(parser); pm_token_t module_keyword = parser->previous; @@ -19150,15 +19351,17 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b accept2(parser, PM_TOKEN_SEMICOLON, PM_TOKEN_NEWLINE); pm_node_t *statements = NULL; - if (!match3(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_END)) { + if (!match4(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_ELSE, PM_TOKEN_KEYWORD_END)) { pm_accepts_block_stack_push(parser, true); statements = (pm_node_t *) parse_statements(parser, PM_CONTEXT_MODULE); pm_accepts_block_stack_pop(parser); } - if (match2(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE)) { + if (match3(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE, PM_TOKEN_KEYWORD_ELSE)) { assert(statements == NULL || PM_NODE_TYPE_P(statements, PM_STATEMENTS_NODE)); - statements = (pm_node_t *) parse_rescues_implicit_begin(parser, module_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_MODULE); + statements = (pm_node_t *) parse_rescues_implicit_begin(parser, opening_newline_index, &module_keyword, module_keyword.start, (pm_statements_node_t *) statements, PM_RESCUES_MODULE); + } else { + parser_warn_indentation_mismatch(parser, opening_newline_index, &module_keyword, false); } pm_constant_id_list_t locals; @@ -19202,6 +19405,8 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b parser_lex(parser); return (pm_node_t *) pm_true_node_create(parser, &parser->previous); case PM_TOKEN_KEYWORD_UNTIL: { + size_t opening_newline_index = token_newline_index(parser); + context_push(parser, PM_CONTEXT_LOOP_PREDICATE); pm_do_loop_stack_push(parser, true); @@ -19215,17 +19420,21 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b expect3(parser, PM_TOKEN_KEYWORD_DO_LOOP, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON, PM_ERR_CONDITIONAL_UNTIL_PREDICATE); pm_statements_node_t *statements = NULL; - if (!accept1(parser, PM_TOKEN_KEYWORD_END)) { + if (!match1(parser, PM_TOKEN_KEYWORD_END)) { pm_accepts_block_stack_push(parser, true); statements = parse_statements(parser, PM_CONTEXT_UNTIL); pm_accepts_block_stack_pop(parser); accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON); - expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_UNTIL_TERM); } + parser_warn_indentation_mismatch(parser, opening_newline_index, &keyword, false); + expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_UNTIL_TERM); + return (pm_node_t *) pm_until_node_create(parser, &keyword, &parser->previous, predicate, statements, 0); } case PM_TOKEN_KEYWORD_WHILE: { + size_t opening_newline_index = token_newline_index(parser); + context_push(parser, PM_CONTEXT_LOOP_PREDICATE); pm_do_loop_stack_push(parser, true); @@ -19239,14 +19448,16 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b expect3(parser, PM_TOKEN_KEYWORD_DO_LOOP, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON, PM_ERR_CONDITIONAL_WHILE_PREDICATE); pm_statements_node_t *statements = NULL; - if (!accept1(parser, PM_TOKEN_KEYWORD_END)) { + if (!match1(parser, PM_TOKEN_KEYWORD_END)) { pm_accepts_block_stack_push(parser, true); statements = parse_statements(parser, PM_CONTEXT_WHILE); pm_accepts_block_stack_pop(parser); accept2(parser, PM_TOKEN_NEWLINE, PM_TOKEN_SEMICOLON); - expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_WHILE_TERM); } + parser_warn_indentation_mismatch(parser, opening_newline_index, &keyword, false); + expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_WHILE_TERM); + return (pm_node_t *) pm_while_node_create(parser, &keyword, &parser->previous, predicate, statements, 0); } case PM_TOKEN_PERCENT_LOWER_I: { @@ -19887,6 +20098,7 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b int previous_lambda_enclosure_nesting = parser->lambda_enclosure_nesting; parser->lambda_enclosure_nesting = parser->enclosure_nesting; + size_t opening_newline_index = token_newline_index(parser); pm_accepts_block_stack_push(parser, true); parser_lex(parser); @@ -19932,10 +20144,12 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b if (accept1(parser, PM_TOKEN_LAMBDA_BEGIN)) { opening = parser->previous; - if (!accept1(parser, PM_TOKEN_BRACE_RIGHT)) { + if (!match1(parser, PM_TOKEN_BRACE_RIGHT)) { body = (pm_node_t *) parse_statements(parser, PM_CONTEXT_LAMBDA_BRACES); - expect1(parser, PM_TOKEN_BRACE_RIGHT, PM_ERR_LAMBDA_TERM_BRACE); } + + parser_warn_indentation_mismatch(parser, opening_newline_index, &operator, false); + expect1(parser, PM_TOKEN_BRACE_RIGHT, PM_ERR_LAMBDA_TERM_BRACE); } else { expect1(parser, PM_TOKEN_KEYWORD_DO, PM_ERR_LAMBDA_OPEN); opening = parser->previous; @@ -19948,7 +20162,9 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b if (match2(parser, PM_TOKEN_KEYWORD_RESCUE, PM_TOKEN_KEYWORD_ENSURE)) { assert(body == NULL || PM_NODE_TYPE_P(body, PM_STATEMENTS_NODE)); - body = (pm_node_t *) parse_rescues_implicit_begin(parser, opening.start, (pm_statements_node_t *) body, PM_RESCUES_LAMBDA); + body = (pm_node_t *) parse_rescues_implicit_begin(parser, opening_newline_index, &operator, opening.start, (pm_statements_node_t *) body, PM_RESCUES_LAMBDA); + } else { + parser_warn_indentation_mismatch(parser, opening_newline_index, &operator, false); } expect1(parser, PM_TOKEN_KEYWORD_END, PM_ERR_LAMBDA_TERM_END); @@ -21473,7 +21689,8 @@ pm_parser_init(pm_parser_t *parser, const uint8_t *source, size_t size, const pm .current_block_exits = NULL, .semantic_token_seen = false, .frozen_string_literal = PM_OPTIONS_FROZEN_STRING_LITERAL_UNSET, - .current_regular_expression_ascii_only = false + .current_regular_expression_ascii_only = false, + .warn_mismatched_indentation = true }; // Initialize the constant pool. We're going to completely guess as to the diff --git a/prism/templates/src/diagnostic.c.erb b/prism/templates/src/diagnostic.c.erb index 064063d9f4..5e6858ac61 100644 --- a/prism/templates/src/diagnostic.c.erb +++ b/prism/templates/src/diagnostic.c.erb @@ -380,9 +380,10 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = { [PM_WARN_END_IN_METHOD] = { "END in method; use at_exit", PM_WARNING_LEVEL_DEFAULT }, [PM_WARN_FLOAT_OUT_OF_RANGE] = { "Float %.*s%s out of range", PM_WARNING_LEVEL_VERBOSE }, [PM_WARN_IGNORED_FROZEN_STRING_LITERAL] = { "'frozen_string_literal' is ignored after any tokens", PM_WARNING_LEVEL_VERBOSE }, + [PM_WARN_INDENTATION_MISMATCH] = { "mismatched indentations at '%.*s' with '%.*s' at %" PRIi32, PM_WARNING_LEVEL_VERBOSE }, [PM_WARN_INTEGER_IN_FLIP_FLOP] = { "integer literal in flip-flop", PM_WARNING_LEVEL_DEFAULT }, [PM_WARN_INVALID_CHARACTER] = { "invalid character syntax; use %s%s%s", PM_WARNING_LEVEL_DEFAULT }, - [PM_WARN_INVALID_SHAREABLE_CONSTANT_VALUE] = { "invalid value for shareable_constant_value: %.*s", PM_WARNING_LEVEL_VERBOSE }, + [PM_WARN_INVALID_MAGIC_COMMENT_VALUE] = { "invalid value for %.*s: %.*s", PM_WARNING_LEVEL_VERBOSE }, [PM_WARN_INVALID_NUMBERED_REFERENCE] = { "'%.*s' is too big for a number variable, always nil", PM_WARNING_LEVEL_DEFAULT }, [PM_WARN_KEYWORD_EOL] = { "`%.*s` at the end of line without an expression", PM_WARNING_LEVEL_VERBOSE }, [PM_WARN_LITERAL_IN_CONDITION_DEFAULT] = { "%sliteral in %s", PM_WARNING_LEVEL_DEFAULT }, diff --git a/test/prism/result/warnings_test.rb b/test/prism/result/warnings_test.rb index c147616a6a..8ccaec74ed 100644 --- a/test/prism/result/warnings_test.rb +++ b/test/prism/result/warnings_test.rb @@ -71,6 +71,64 @@ module Prism assert_warning("_ = 1.0e100000", "out of range") end + def test_indentation_mismatch + assert_warning("if true\n end", "mismatched indentations at 'end' with 'if'") + assert_warning("if true\n elsif true\nend", "mismatched indentations at 'elsif' with 'if'") + assert_warning("if true\n else\nend", "mismatched indentations at 'else' with 'if'", "mismatched indentations at 'end' with 'else'") + + assert_warning("unless true\n end", "mismatched indentations at 'end' with 'unless'") + assert_warning("unless true\n else\nend", "mismatched indentations at 'else' with 'unless'", "mismatched indentations at 'end' with 'else'") + + assert_warning("while true\n end", "mismatched indentations at 'end' with 'while'") + assert_warning("until true\n end", "mismatched indentations at 'end' with 'until'") + + assert_warning("begin\n end", "mismatched indentations at 'end' with 'begin'") + assert_warning("begin\n rescue\nend", "mismatched indentations at 'rescue' with 'begin'") + assert_warning("begin\n ensure\nend", "mismatched indentations at 'ensure' with 'begin'") + assert_warning("begin\nrescue\n else\nend", "mismatched indentations at 'else' with 'begin'", "mismatched indentations at 'end' with 'else'") + assert_warning("begin\n rescue\n ensure\n end", "mismatched indentations at 'rescue' with 'begin'", "mismatched indentations at 'ensure' with 'begin'", "mismatched indentations at 'end' with 'begin'"); + + assert_warning("def foo\n end", "mismatched indentations at 'end' with 'def'") + assert_warning("def foo\n rescue\nend", "mismatched indentations at 'rescue' with 'def'") + assert_warning("def foo\n ensure\nend", "mismatched indentations at 'ensure' with 'def'") + assert_warning("def foo\nrescue\n else\nend", "mismatched indentations at 'else' with 'def'", "mismatched indentations at 'end' with 'else'") + assert_warning("def foo\n rescue\n ensure\n end", "mismatched indentations at 'rescue' with 'def'", "mismatched indentations at 'ensure' with 'def'", "mismatched indentations at 'end' with 'def'"); + + assert_warning("class Foo\n end", "mismatched indentations at 'end' with 'class'") + assert_warning("class Foo\n rescue\nend", "mismatched indentations at 'rescue' with 'class'") + assert_warning("class Foo\n ensure\nend", "mismatched indentations at 'ensure' with 'class'") + assert_warning("class Foo\nrescue\n else\nend", "mismatched indentations at 'else' with 'class'", "mismatched indentations at 'end' with 'else'") + assert_warning("class Foo\n rescue\n ensure\n end", "mismatched indentations at 'rescue' with 'class'", "mismatched indentations at 'ensure' with 'class'", "mismatched indentations at 'end' with 'class'"); + + assert_warning("module Foo\n end", "mismatched indentations at 'end' with 'module'") + assert_warning("module Foo\n rescue\nend", "mismatched indentations at 'rescue' with 'module'") + assert_warning("module Foo\n ensure\nend", "mismatched indentations at 'ensure' with 'module'") + assert_warning("module Foo\nrescue\n else\nend", "mismatched indentations at 'else' with 'module'", "mismatched indentations at 'end' with 'else'") + assert_warning("module Foo\n rescue\n ensure\n end", "mismatched indentations at 'rescue' with 'module'", "mismatched indentations at 'ensure' with 'module'", "mismatched indentations at 'end' with 'module'"); + + assert_warning("class << foo\n end", "mismatched indentations at 'end' with 'class'") + assert_warning("class << foo\n rescue\nend", "mismatched indentations at 'rescue' with 'class'") + assert_warning("class << foo\n ensure\nend", "mismatched indentations at 'ensure' with 'class'") + assert_warning("class << foo\nrescue\n else\nend", "mismatched indentations at 'else' with 'class'", "mismatched indentations at 'end' with 'else'") + assert_warning("class << foo\n rescue\n ensure\n end", "mismatched indentations at 'rescue' with 'class'", "mismatched indentations at 'ensure' with 'class'", "mismatched indentations at 'end' with 'class'"); + + assert_warning("case 1; when 2\n end", "mismatched indentations at 'end' with 'case'") + assert_warning("case 1; in 2\n end", "mismatched indentations at 'end' with 'case'") + + assert_warning("-> {\n }", "mismatched indentations at '}' with '->'") + assert_warning("-> do\n end", "mismatched indentations at 'end' with '->'") + assert_warning("-> do\n rescue\nend", "mismatched indentations at 'rescue' with '->'") + assert_warning("-> do\n ensure\nend", "mismatched indentations at 'ensure' with '->'") + assert_warning("-> do\nrescue\n else\nend", "mismatched indentations at 'else' with '->'", "mismatched indentations at 'end' with 'else'") + assert_warning("-> do\n rescue\n ensure\n end", "mismatched indentations at 'rescue' with '->'", "mismatched indentations at 'ensure' with '->'", "mismatched indentations at 'end' with '->'"); + assert_warning("foo do\nrescue\n else\nend", "mismatched indentations at 'end' with 'else'") + + refute_warning("class Foo; end") # same line + refute_warning("; class Foo\nend") # non whitespace on opening line + refute_warning("\tclass Foo\n end") # tab stop matches space + refute_warning(" \tclass Foo\n end") # tab stop matches space + end + def test_integer_in_flip_flop assert_warning("1 if 2..foo", "integer") end @@ -275,14 +333,17 @@ module Prism private - def assert_warning(source, message) + def assert_warning(source, *messages) warnings = Prism.parse(source).warnings + assert_equal messages.length, warnings.length, "Expected #{messages.length} warning(s) in #{source.inspect}, got #{warnings.map(&:message).inspect}" - assert_equal 1, warnings.length, "Expected only one warning in #{source.inspect}, got #{warnings.map(&:message).inspect}" - assert_include warnings.first.message, message + warnings.zip(messages).each do |warning, message| + assert_include warning.message, message + end if defined?(RubyVM::AbstractSyntaxTree) - assert_include capture_warning { RubyVM::AbstractSyntaxTree.parse(source) }, message + stderr = capture_stderr { RubyVM::AbstractSyntaxTree.parse(source) } + messages.each { |message| assert_include stderr, message } end end @@ -290,11 +351,11 @@ module Prism assert_empty Prism.parse(source, **options).warnings if compare && defined?(RubyVM::AbstractSyntaxTree) - assert_empty capture_warning { RubyVM::AbstractSyntaxTree.parse(source) } + assert_empty capture_stderr { RubyVM::AbstractSyntaxTree.parse(source) } end end - def capture_warning + def capture_stderr stderr, $stderr, verbose, $VERBOSE = $stderr, StringIO.new, $VERBOSE, true begin |