diff --git a/Units/parser-ruby.r/ruby-block-assign.d/expected.tags b/Units/parser-ruby.r/ruby-block-assign.d/expected.tags new file mode 100644 index 0000000000..2c1dcc2df4 --- /dev/null +++ b/Units/parser-ruby.r/ruby-block-assign.d/expected.tags @@ -0,0 +1,10 @@ +Bar input.rb /^c = class Bar$/;" c +Foo input.rb /^m = module Foo$/;" m +Quux input.rb /^m += module Quux$/;" m +Zook input.rb /^c ||= class Zook$/;" c +method_a input.rb /^ x = def method_a$/;" f class:Bar +method_b input.rb /^ def method_b$/;" f class:Bar +method_c input.rb /^ def method_c$/;" f class:Bar +method_d input.rb /^ def method_d$/;" f class:Bar +method_e input.rb /^ def method_e$/;" f class:Bar +method_f input.rb /^ def method_f$/;" f class:Bar diff --git a/Units/parser-ruby.r/ruby-block-assign.d/input.rb b/Units/parser-ruby.r/ruby-block-assign.d/input.rb new file mode 100644 index 0000000000..5b2d3ab684 --- /dev/null +++ b/Units/parser-ruby.r/ruby-block-assign.d/input.rb @@ -0,0 +1,43 @@ +m = module Foo +end + +m += module Quux +end + +c = class Bar + x = def method_a + if 1 + else + end + end + + def method_b + x = while 1 do + break + end + end + + def method_c + x = if 1 + else + end + end + + def method_d + x += if 1 + else + end + end + + def method_e + x ||= if 1 + else + end + end + + def method_f + end +end + +c ||= class Zook +end diff --git a/parsers/ruby.c b/parsers/ruby.c index 9f7379e1fd..67a57dc9c8 100644 --- a/parsers/ruby.c +++ b/parsers/ruby.c @@ -25,11 +25,6 @@ #include "routines.h" #include "vstring.h" -/* - * MACROS - */ -#define isIdentChar(c) (isalnum (c) || (c) == '_') - /* * DATA DECLARATIONS */ @@ -117,19 +112,29 @@ static bool canMatch (const unsigned char** s, const char* literal, return true; } +static bool isIdentChar (int c) +{ + return (isalnum (c) || c == '_'); +} + static bool notIdentChar (int c) { return ! isIdentChar (c); } +static bool operatorChar (int c) +{ + return (c == '[' || c == ']' || + c == '=' || c == '!' || c == '~' || + c == '+' || c == '-' || + c == '@' || c == '*' || c == '/' || c == '%' || + c == '<' || c == '>' || + c == '&' || c == '^' || c == '|'); +} + static bool notOperatorChar (int c) { - return ! (c == '[' || c == ']' || - c == '=' || c == '!' || c == '~' || - c == '+' || c == '-' || - c == '@' || c == '*' || c == '/' || c == '%' || - c == '<' || c == '>' || - c == '&' || c == '^' || c == '|'); + return ! operatorChar (c); } static bool isWhitespace (int c) @@ -137,11 +142,70 @@ static bool isWhitespace (int c) return c == 0 || isspace (c); } +/* + * Advance 's' while the passed predicate is true. Returns true if + * advanced by at least one position. + */ +static bool advanceWhile (const unsigned char** s, bool (*predicate) (int)) +{ + const unsigned char* original_pos = *s; + + while (**s != '\0') + { + if (! predicate (**s)) + { + return *s != original_pos; + } + + (*s)++; + } + + return *s != original_pos; +} + static bool canMatchKeyword (const unsigned char** s, const char* literal) { return canMatch (s, literal, notIdentChar); } +/* + * Extends canMatch. Works similarly, but allows assignment to precede + * the keyword, as block assignment is a common Ruby idiom. + */ +static bool canMatchKeywordWithAssign (const unsigned char** s, const char* literal) +{ + const unsigned char* original_pos = *s; + + if (canMatchKeyword (s, literal)) + { + return true; + } + + if (! advanceWhile (s, isIdentChar)) + { + *s = original_pos; + return false; + } + + advanceWhile (s, isWhitespace); + + if (! (advanceWhile (s, operatorChar) && *(*s - 1) == '=')) + { + *s = original_pos; + return false; + } + + advanceWhile (s, isWhitespace); + + if (canMatchKeyword (s, literal)) + { + return true; + } + + *s = original_pos; + return false; +} + /* * Attempts to advance 'cp' past a Ruby operator method name. Returns * true if successful (and copies the name into 'name'), false otherwise. @@ -428,29 +492,22 @@ static void findRubyTags (void) * * return if * - * FIXME: this is fooled by code such as - * - * result = if - * - * else - * - * end - * - * FIXME: we're also fooled if someone does something heinous such as + * FIXME: we're fooled if someone does something heinous such as * * puts("hello") \ * unless */ - if (canMatchKeyword (&cp, "for") || - canMatchKeyword (&cp, "until") || - canMatchKeyword (&cp, "while")) + + if (canMatchKeywordWithAssign (&cp, "for") || + canMatchKeywordWithAssign (&cp, "until") || + canMatchKeywordWithAssign (&cp, "while")) { expect_separator = true; enterUnnamedScope (); } - else if (canMatchKeyword (&cp, "case") || - canMatchKeyword (&cp, "if") || - canMatchKeyword (&cp, "unless")) + else if (canMatchKeywordWithAssign (&cp, "case") || + canMatchKeywordWithAssign (&cp, "if") || + canMatchKeywordWithAssign (&cp, "unless")) { enterUnnamedScope (); } @@ -459,15 +516,15 @@ static void findRubyTags (void) * "module M", "class C" and "def m" should only be at the beginning * of a line. */ - if (canMatchKeyword (&cp, "module")) + if (canMatchKeywordWithAssign (&cp, "module")) { readAndEmitTag (&cp, K_MODULE); } - else if (canMatchKeyword (&cp, "class")) + else if (canMatchKeywordWithAssign (&cp, "class")) { readAndEmitTag (&cp, K_CLASS); } - else if (canMatchKeyword (&cp, "def")) + else if (canMatchKeywordWithAssign (&cp, "def")) { rubyKind kind = K_METHOD; NestingLevel *nl = nestingLevelsGetCurrent (nesting);