From daac49611e5195b843114c0b338092a9611c8171 Mon Sep 17 00:00:00 2001 From: schneems Date: Mon, 11 Oct 2021 14:30:40 -0500 Subject: [PATCH] [close #75] Support endless method definition When there is an "endless" method definition, we don't want to count it as having a keyword, otherwise when we try to expand it will search for a matching `end` an possibly introduce a problem in the search. To detect this case I use the same logic introduced in https://github.com/ruby/irb/commit/826ae909c9c93a2ddca6f9cfcd9c94dbf53d44ab For reference here are lex of a regular method definition (first line only) and then an "endless" definition: ``` irb(main):015:0> Ripper.lex("def square(x)") => [[[1, 0], :on_kw, "def", FNAME], [[1, 3], :on_sp, " ", FNAME], [[1, 4], :on_ident, "square", ENDFN], [[1, 10], :on_lparen, "(", BEG|LABEL], [[1, 11], :on_ident, "x", ARG], [[1, 12], :on_rparen, ")", ENDFN]] irb(main):016:0> Ripper.lex("def square(x) = x * x") => [[[1, 0], :on_kw, "def", FNAME], [[1, 3], :on_sp, " ", FNAME], [[1, 4], :on_ident, "square", ENDFN], [[1, 10], :on_lparen, "(", BEG|LABEL], [[1, 11], :on_ident, "x", ARG], [[1, 12], :on_rparen, ")", ENDFN], [[1, 13], :on_sp, " ", BEG], [[1, 14], :on_op, "=", BEG], [[1, 15], :on_sp, " ", BEG], [[1, 16], :on_ident, "x", END|LABEL], [[1, 17], :on_sp, " ", END|LABEL], [[1, 18], :on_op, "*", BEG], [[1, 19], :on_sp, " ", BEG], [[1, 20], :on_ident, "x", END|LABEL]] ``` The detection uses a state machine. It knows when it sees an `ENDFN` state that a function is being defined, but it's not known if it's a "regular" function or not. If the `ENDFN` is followed by a `BEG` with a token of `=` and then a `END` then it's assumed to be a full "oneliner" AKA "endless" method definition. --- CHANGELOG.md | 1 + lib/dead_end/code_line.rb | 34 ++++++++++++++++++++++++++++++++++ spec/unit/code_line_spec.rb | 11 +++++++++++ 3 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be01d2e..ba84cde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## HEAD (unreleased) +- Support "endless" oneline method definitions for Ruby 3+ (https://github.com/zombocom/dead_end/pull/80) - Reduce timeout to 1 second (https://github.com/zombocom/dead_end/pull/79) - Logically consecutive lines (such as chained methods are now joined) (https://github.com/zombocom/dead_end/pull/78) - Output improvement for cases where the only line is an single `end` (https://github.com/zombocom/dead_end/pull/78) diff --git a/lib/dead_end/code_line.rb b/lib/dead_end/code_line.rb index ebf8a49..8718fdb 100644 --- a/lib/dead_end/code_line.rb +++ b/lib/dead_end/code_line.rb @@ -60,6 +60,8 @@ def initialize(line:, index:, lex:) end_count += 1 if lex.is_end? end + kw_count -= oneliner_method_count + @is_kw = (kw_count - end_count) > 0 @is_end = (end_count - kw_count) > 0 end @@ -195,5 +197,37 @@ def trailing_slash? last.token == TRAILING_SLASH end + + # Endless method detection + # + # From https://github.com/ruby/irb/commit/826ae909c9c93a2ddca6f9cfcd9c94dbf53d44ab + # Detecting a "oneliner" seems to need a state machine. + # This can be done by looking mostly at the "state" (last value): + # + # ENDFN -> BEG (token = '=' ) -> END + # + private def oneliner_method_count + oneliner_count = 0 + in_oneliner_def = nil + + @lex.each do |lex| + if in_oneliner_def.nil? + in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN) + elsif lex.state.allbits?(Ripper::EXPR_ENDFN) + # Continue + elsif lex.state.allbits?(Ripper::EXPR_BEG) + in_oneliner_def = :BODY if lex.token == "=" + elsif lex.state.allbits?(Ripper::EXPR_END) + # We found an endless method, count it + oneliner_count += 1 if in_oneliner_def == :BODY + + in_oneliner_def = nil + else + in_oneliner_def = nil + end + end + + oneliner_count + end end end diff --git a/spec/unit/code_line_spec.rb b/spec/unit/code_line_spec.rb index 29324cf..1eb1a86 100644 --- a/spec/unit/code_line_spec.rb +++ b/spec/unit/code_line_spec.rb @@ -4,6 +4,17 @@ module DeadEnd RSpec.describe CodeLine do + it "supports endless method definitions" do + skip("Unsupported ruby version") unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3") + + line = CodeLine.from_source(<<~'EOM').first + def square(x) = x * x + EOM + + expect(line.is_kw?).to be_falsey + expect(line.is_end?).to be_falsey + end + it "retains original line value, after being marked invisible" do line = CodeLine.from_source(<<~'EOM').first puts "lol"