Skip to content

Commit 0781e55

Browse files
st0012matzbot
authored andcommitted
[ruby/irb] Move assignment check to RubyLex
(ruby/irb#670) Since assignment check relies on tokenization with `Ripper`, it feels like the responsibility of `RubyLex`. `Irb#eval_input` should simply get the result when calling `each_top_level_statement` on `RubyLex`. ruby/irb@89d1adb3fd
1 parent c173c63 commit 0781e55

File tree

4 files changed

+98
-99
lines changed

4 files changed

+98
-99
lines changed

lib/irb.rb

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -429,30 +429,6 @@ def IRB.irb_abort(irb, exception = Abort)
429429
end
430430

431431
class Irb
432-
ASSIGNMENT_NODE_TYPES = [
433-
# Local, instance, global, class, constant, instance, and index assignment:
434-
# "foo = bar",
435-
# "@foo = bar",
436-
# "$foo = bar",
437-
# "@@foo = bar",
438-
# "::Foo = bar",
439-
# "a::Foo = bar",
440-
# "Foo = bar"
441-
# "foo.bar = 1"
442-
# "foo[1] = bar"
443-
:assign,
444-
445-
# Operation assignment:
446-
# "foo += bar"
447-
# "foo -= bar"
448-
# "foo ||= bar"
449-
# "foo &&= bar"
450-
:opassign,
451-
452-
# Multiple assignment:
453-
# "foo, bar = 1, 2
454-
:massign,
455-
]
456432
# Note: instance and index assignment expressions could also be written like:
457433
# "foo.bar=(1)" and "foo.[]=(1, bar)", when expressed that way, the former
458434
# be parsed as :assign and echo will be suppressed, but the latter is
@@ -563,11 +539,9 @@ def eval_input
563539

564540
@scanner.configure_io(@context.io)
565541

566-
@scanner.each_top_level_statement do |line, line_no|
542+
@scanner.each_top_level_statement do |line, line_no, is_assignment|
567543
signal_status(:IN_EVAL) do
568544
begin
569-
# Assignment expression check should be done before evaluate_line to handle code like `a /2#/ if false; a = 1`
570-
is_assignment = assignment_expression?(line)
571545
evaluate_line(line, line_no)
572546

573547
# Don't echo if the line ends with a semicolon
@@ -876,24 +850,6 @@ def inspect
876850
end
877851
format("#<%s: %s>", self.class, ary.join(", "))
878852
end
879-
880-
def assignment_expression?(line)
881-
# Try to parse the line and check if the last of possibly multiple
882-
# expressions is an assignment type.
883-
884-
# If the expression is invalid, Ripper.sexp should return nil which will
885-
# result in false being returned. Any valid expression should return an
886-
# s-expression where the second element of the top level array is an
887-
# array of parsed expressions. The first element of each expression is the
888-
# expression's type.
889-
verbose, $VERBOSE = $VERBOSE, nil
890-
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{line}"
891-
# Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
892-
node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
893-
ASSIGNMENT_NODE_TYPES.include?(node_type)
894-
ensure
895-
$VERBOSE = verbose
896-
end
897853
end
898854

899855
def @CONF.inspect

lib/irb/ruby-lex.rb

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@
1010

1111
# :stopdoc:
1212
class RubyLex
13+
ASSIGNMENT_NODE_TYPES = [
14+
# Local, instance, global, class, constant, instance, and index assignment:
15+
# "foo = bar",
16+
# "@foo = bar",
17+
# "$foo = bar",
18+
# "@@foo = bar",
19+
# "::Foo = bar",
20+
# "a::Foo = bar",
21+
# "Foo = bar"
22+
# "foo.bar = 1"
23+
# "foo[1] = bar"
24+
:assign,
25+
26+
# Operation assignment:
27+
# "foo += bar"
28+
# "foo -= bar"
29+
# "foo ||= bar"
30+
# "foo &&= bar"
31+
:opassign,
32+
33+
# Multiple assignment:
34+
# "foo, bar = 1, 2
35+
:massign,
36+
]
1337

1438
class TerminateLineInput < StandardError
1539
def initialize
@@ -248,13 +272,31 @@ def each_top_level_statement
248272

249273
if code != "\n"
250274
code.force_encoding(@io.encoding)
251-
yield code, @line_no
275+
yield code, @line_no, assignment_expression?(code)
252276
end
253277
@line_no += code.count("\n")
254278
rescue TerminateLineInput
255279
end
256280
end
257281

282+
def assignment_expression?(line)
283+
# Try to parse the line and check if the last of possibly multiple
284+
# expressions is an assignment type.
285+
286+
# If the expression is invalid, Ripper.sexp should return nil which will
287+
# result in false being returned. Any valid expression should return an
288+
# s-expression where the second element of the top level array is an
289+
# array of parsed expressions. The first element of each expression is the
290+
# expression's type.
291+
verbose, $VERBOSE = $VERBOSE, nil
292+
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{line}"
293+
# Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
294+
node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
295+
ASSIGNMENT_NODE_TYPES.include?(node_type)
296+
ensure
297+
$VERBOSE = verbose
298+
end
299+
258300
def should_continue?(tokens)
259301
# Look at the last token and check if IRB need to continue reading next line.
260302
# Example code that should continue: `a\` `a +` `a.`

test/irb/test_context.rb

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -202,59 +202,6 @@ def test_default_config
202202
assert_equal(true, @context.use_autocomplete?)
203203
end
204204

205-
def test_assignment_expression
206-
input = TestInputMethod.new
207-
irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input)
208-
[
209-
"foo = bar",
210-
"@foo = bar",
211-
"$foo = bar",
212-
"@@foo = bar",
213-
"::Foo = bar",
214-
"a::Foo = bar",
215-
"Foo = bar",
216-
"foo.bar = 1",
217-
"foo[1] = bar",
218-
"foo += bar",
219-
"foo -= bar",
220-
"foo ||= bar",
221-
"foo &&= bar",
222-
"foo, bar = 1, 2",
223-
"foo.bar=(1)",
224-
"foo; foo = bar",
225-
"foo; foo = bar; ;\n ;",
226-
"foo\nfoo = bar",
227-
].each do |exp|
228-
assert(
229-
irb.assignment_expression?(exp),
230-
"#{exp.inspect}: should be an assignment expression"
231-
)
232-
end
233-
234-
[
235-
"foo",
236-
"foo.bar",
237-
"foo[0]",
238-
"foo = bar; foo",
239-
"foo = bar\nfoo",
240-
].each do |exp|
241-
refute(
242-
irb.assignment_expression?(exp),
243-
"#{exp.inspect}: should not be an assignment expression"
244-
)
245-
end
246-
end
247-
248-
def test_assignment_expression_with_local_variable
249-
input = TestInputMethod.new
250-
irb = IRB::Irb.new(IRB::WorkSpace.new(Object.new), input)
251-
code = "a /1;x=1#/"
252-
refute(irb.assignment_expression?(code), "#{code}: should not be an assignment expression")
253-
irb.context.workspace.binding.eval('a = 1')
254-
assert(irb.assignment_expression?(code), "#{code}: should be an assignment expression")
255-
refute(irb.assignment_expression?(""), "empty code should not be an assignment expression")
256-
end
257-
258205
def test_echo_on_assignment
259206
input = TestInputMethod.new([
260207
"a = 1\n",

test/irb/test_ruby_lex.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,60 @@ def test_indent_level_with_heredoc_and_embdoc
813813
assert_indent_level(code_with_embdoc.lines, expected)
814814
end
815815

816+
def test_assignment_expression
817+
context = build_context
818+
ruby_lex = RubyLex.new(context)
819+
820+
[
821+
"foo = bar",
822+
"@foo = bar",
823+
"$foo = bar",
824+
"@@foo = bar",
825+
"::Foo = bar",
826+
"a::Foo = bar",
827+
"Foo = bar",
828+
"foo.bar = 1",
829+
"foo[1] = bar",
830+
"foo += bar",
831+
"foo -= bar",
832+
"foo ||= bar",
833+
"foo &&= bar",
834+
"foo, bar = 1, 2",
835+
"foo.bar=(1)",
836+
"foo; foo = bar",
837+
"foo; foo = bar; ;\n ;",
838+
"foo\nfoo = bar",
839+
].each do |exp|
840+
assert(
841+
ruby_lex.assignment_expression?(exp),
842+
"#{exp.inspect}: should be an assignment expression"
843+
)
844+
end
845+
846+
[
847+
"foo",
848+
"foo.bar",
849+
"foo[0]",
850+
"foo = bar; foo",
851+
"foo = bar\nfoo",
852+
].each do |exp|
853+
refute(
854+
ruby_lex.assignment_expression?(exp),
855+
"#{exp.inspect}: should not be an assignment expression"
856+
)
857+
end
858+
end
859+
860+
def test_assignment_expression_with_local_variable
861+
context = build_context
862+
ruby_lex = RubyLex.new(context)
863+
code = "a /1;x=1#/"
864+
refute(ruby_lex.assignment_expression?(code), "#{code}: should not be an assignment expression")
865+
context.workspace.binding.eval('a = 1')
866+
assert(ruby_lex.assignment_expression?(code), "#{code}: should be an assignment expression")
867+
refute(ruby_lex.assignment_expression?(""), "empty code should not be an assignment expression")
868+
end
869+
816870
private
817871

818872
def build_context(local_variables = nil)

0 commit comments

Comments
 (0)