Skip to content

Commit

Permalink
Clear all context usages in RubyLex
Browse files Browse the repository at this point in the history
After this change, `RubyLex` will not interact with `Context` directly
in any way. This decoupling has a few benefits:

- It makes `RubyLex` easier to test as it no longer has a dependency on
  `Context`. We can see this from the removal of `build_context` from
  `test_ruby_lex.rb`.
- It will make `RubyLex` easier to understand as it will not be affected
  by state changes in `Context` objects.
- It allows `RubyLex` to be used in places where `Context` is not available.
  • Loading branch information
st0012 committed Oct 4, 2023
1 parent 5d67967 commit 4bdf0a0
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 54 deletions.
15 changes: 8 additions & 7 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ def initialize(workspace = nil, input_method = nil)
@context = Context.new(self, workspace, input_method)
@context.workspace.load_commands_to_main
@signal_status = :IN_IRB
@scanner = RubyLex.new(@context)
@scanner = RubyLex.new
end

# A hook point for `debug` command's breakpoint after :IRB_EXIT as well as its clean-up
Expand Down Expand Up @@ -610,7 +610,7 @@ def readmultiline
# Accept any single-line input for symbol aliases or commands that transform args
return code if single_line_command?(code)

tokens, opens, terminated = @scanner.check_code_state(code)
tokens, opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables)
return code if terminated

line_offset += 1
Expand Down Expand Up @@ -643,7 +643,8 @@ def build_statement(code)
if command_class
Statement::Command.new(code, command, arg, command_class)
else
Statement::Expression.new(code, @scanner.assignment_expression?(code))
is_assignment_expression = @scanner.assignment_expression?(code, local_variables: @context.local_variables)
Statement::Expression.new(code, is_assignment_expression)
end
end

Expand All @@ -656,7 +657,7 @@ def configure_io
if @context.io.respond_to?(:check_termination)
@context.io.check_termination do |code|
if Reline::IOGate.in_pasting?
rest = @scanner.check_termination_in_prev_line(code)
rest = @scanner.check_termination_in_prev_line(code, local_variables: @context.local_variables)
if rest
Reline.delete_text
rest.bytes.reverse_each do |c|
Expand All @@ -670,15 +671,15 @@ def configure_io
# Accept any single-line input for symbol aliases or commands that transform args
next true if single_line_command?(code)

_tokens, _opens, terminated = @scanner.check_code_state(code)
_tokens, _opens, terminated = @scanner.check_code_state(code, local_variables: @context.local_variables)
terminated
end
end
end
if @context.io.respond_to?(:dynamic_prompt)
@context.io.dynamic_prompt do |lines|
lines << '' if lines.empty?
tokens = RubyLex.ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join, context: @context)
tokens = RubyLex.ripper_lex_without_warning(lines.map{ |l| l + "\n" }.join, local_variables: @context.local_variables)
line_results = IRB::NestingParser.parse_by_line(tokens)
tokens_until_line = []
line_results.map.with_index do |(line_tokens, _prev_opens, next_opens, _min_depth), line_num_offset|
Expand All @@ -698,7 +699,7 @@ def configure_io
next nil if !is_newline && lines[line_index]&.byteslice(0, byte_pointer)&.match?(/\A\s*\z/)

code = lines[0..line_index].map { |l| "#{l}\n" }.join
tokens = RubyLex.ripper_lex_without_warning(code, context: @context)
tokens = RubyLex.ripper_lex_without_warning(code, local_variables: @context.local_variables)
@scanner.process_indent_level(tokens, lines, line_index, is_newline)
end
end
Expand Down
31 changes: 15 additions & 16 deletions lib/irb/ruby-lex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ def initialize

attr_reader :line_no

def initialize(context)
@context = context
def initialize
@line_no = 1
@prompt = nil
end
Expand Down Expand Up @@ -116,9 +115,9 @@ def self.interpolate_ripper_ignored_tokens(code, tokens)
interpolated
end

def self.ripper_lex_without_warning(code, context: nil)
def self.ripper_lex_without_warning(code, local_variables: [])
verbose, $VERBOSE = $VERBOSE, nil
lvars_code = generate_local_variables_assign_code(context&.local_variables || [])
lvars_code = generate_local_variables_assign_code(local_variables)
original_code = code
if lvars_code
code = "#{lvars_code}\n#{code}"
Expand Down Expand Up @@ -152,14 +151,14 @@ def prompt(opens, continue, line_num_offset)
@prompt&.call(ltype, indent_level, opens.any? || continue, @line_no + line_num_offset)
end

def check_code_state(code)
tokens = self.class.ripper_lex_without_warning(code, context: @context)
def check_code_state(code, local_variables:)
tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
opens = NestingParser.open_tokens(tokens)
[tokens, opens, code_terminated?(code, tokens, opens)]
[tokens, opens, code_terminated?(code, tokens, opens, local_variables: local_variables)]
end

def code_terminated?(code, tokens, opens)
case check_code_syntax(code)
def code_terminated?(code, tokens, opens, local_variables:)
case check_code_syntax(code, local_variables: local_variables)
when :unrecoverable_error
true
when :recoverable_error
Expand All @@ -180,7 +179,7 @@ def increase_line_no(addition)
@line_no += addition
end

def assignment_expression?(code)
def assignment_expression?(code, local_variables:)
# Try to parse the code and check if the last of possibly multiple
# expressions is an assignment type.

Expand All @@ -190,7 +189,7 @@ def assignment_expression?(code)
# array of parsed expressions. The first element of each expression is the
# expression's type.
verbose, $VERBOSE = $VERBOSE, nil
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{code}"
code = "#{RubyLex.generate_local_variables_assign_code(local_variables) || 'nil;'}\n#{code}"
# Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
ASSIGNMENT_NODE_TYPES.include?(node_type)
Expand Down Expand Up @@ -222,8 +221,8 @@ def should_continue?(tokens)
false
end

def check_code_syntax(code)
lvars_code = RubyLex.generate_local_variables_assign_code(@context.local_variables)
def check_code_syntax(code, local_variables:)
lvars_code = RubyLex.generate_local_variables_assign_code(local_variables)
code = "#{lvars_code}\n#{code}"

begin # check if parser error are available
Expand Down Expand Up @@ -455,8 +454,8 @@ def ltype_from_open_tokens(opens)
end
end

def check_termination_in_prev_line(code)
tokens = self.class.ripper_lex_without_warning(code, context: @context)
def check_termination_in_prev_line(code, local_variables:)
tokens = self.class.ripper_lex_without_warning(code, local_variables: local_variables)
past_first_newline = false
index = tokens.rindex do |t|
# traverse first token before last line
Expand Down Expand Up @@ -486,7 +485,7 @@ def check_termination_in_prev_line(code)
tokens_without_last_line = tokens[0..index]
code_without_last_line = tokens_without_last_line.map(&:tok).join
opens_without_last_line = NestingParser.open_tokens(tokens_without_last_line)
if code_terminated?(code_without_last_line, tokens_without_last_line, opens_without_last_line)
if code_terminated?(code_without_last_line, tokens_without_last_line, opens_without_last_line, local_variables: local_variables)
return last_line_tokens.map(&:tok).join
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/irb/source_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ def find_source(signature)
private

def find_end(file, first_line)
lex = RubyLex.new(@irb_context)
lex = RubyLex.new
lines = File.read(file).lines[(first_line - 1)..-1]
tokens = RubyLex.ripper_lex_without_warning(lines.join)
tokens = RubyLex.ripper_lex_without_warning(lines.join, local_variables: [])
prev_tokens = []

# chunk with line number
tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
code = lines[0..lnum].join
prev_tokens.concat chunk
continue = lex.should_continue?(prev_tokens)
syntax = lex.check_code_syntax(code)
syntax = lex.check_code_syntax(code, local_variables: [])
if !continue && syntax == :valid
return first_line + lnum
end
Expand Down
2 changes: 1 addition & 1 deletion test/irb/test_irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def assert_rows_with_correct_indents(rows_with_spaces, assert_indent_level: fals

def assert_indent_level(lines, expected)
code = lines.map { |l| "#{l}\n" }.join # code should end with "\n"
_tokens, opens, _ = @irb.scanner.check_code_state(code)
_tokens, opens, _ = @irb.scanner.check_code_state(code, local_variables: [])
indent_level = @irb.scanner.calc_indent_level(opens)
error_message = "Calculated the wrong number of indent level for:\n #{lines.join("\n")}"
assert_equal(expected, indent_level, error_message)
Expand Down
36 changes: 9 additions & 27 deletions test/irb/test_ruby_lex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ def test_indent_level_with_heredoc_and_embdoc
end

def test_assignment_expression
context = build_context
ruby_lex = IRB::RubyLex.new(context)
ruby_lex = IRB::RubyLex.new

[
"foo = bar",
Expand All @@ -173,7 +172,7 @@ def test_assignment_expression
"foo\nfoo = bar",
].each do |exp|
assert(
ruby_lex.assignment_expression?(exp),
ruby_lex.assignment_expression?(exp, local_variables: []),
"#{exp.inspect}: should be an assignment expression"
)
end
Expand All @@ -186,20 +185,18 @@ def test_assignment_expression
"foo = bar\nfoo",
].each do |exp|
refute(
ruby_lex.assignment_expression?(exp),
ruby_lex.assignment_expression?(exp, local_variables: []),
"#{exp.inspect}: should not be an assignment expression"
)
end
end

def test_assignment_expression_with_local_variable
context = build_context
ruby_lex = IRB::RubyLex.new(context)
ruby_lex = IRB::RubyLex.new
code = "a /1;x=1#/"
refute(ruby_lex.assignment_expression?(code), "#{code}: should not be an assignment expression")
context.workspace.binding.eval('a = 1')
assert(ruby_lex.assignment_expression?(code), "#{code}: should be an assignment expression")
refute(ruby_lex.assignment_expression?(""), "empty code should not be an assignment expression")
refute(ruby_lex.assignment_expression?(code, local_variables: []), "#{code}: should not be an assignment expression")
assert(ruby_lex.assignment_expression?(code, local_variables: [:a]), "#{code}: should be an assignment expression")
refute(ruby_lex.assignment_expression?("", local_variables: [:a]), "empty code should not be an assignment expression")
end

def test_initialising_the_old_top_level_ruby_lex
Expand All @@ -211,20 +208,6 @@ def test_initialising_the_old_top_level_ruby_lex

private

def build_context(local_variables = nil)
IRB.init_config(nil)
workspace = IRB::WorkSpace.new(TOPLEVEL_BINDING.dup)

if local_variables
local_variables.each do |n|
workspace.binding.local_variable_set(n, nil)
end
end

IRB.conf[:VERBOSE] = false
IRB::Context.new(nil, workspace, TestInputMethod.new)
end

def assert_indent_level(lines, expected, local_variables: [])
indent_level, _continue, _code_block_open = check_state(lines, local_variables: local_variables)
error_message = "Calculated the wrong number of indent level for:\n #{lines.join("\n")}"
Expand All @@ -244,10 +227,9 @@ def assert_code_block_open(lines, expected, local_variables: [])
end

def check_state(lines, local_variables: [])
context = build_context(local_variables)
code = lines.map { |l| "#{l}\n" }.join # code should end with "\n"
ruby_lex = IRB::RubyLex.new(context)
tokens, opens, terminated = ruby_lex.check_code_state(code)
ruby_lex = IRB::RubyLex.new
tokens, opens, terminated = ruby_lex.check_code_state(code, local_variables: local_variables)
indent_level = ruby_lex.calc_indent_level(opens)
continue = ruby_lex.should_continue?(tokens)
[indent_level, continue, !terminated]
Expand Down

0 comments on commit 4bdf0a0

Please sign in to comment.