seeing_is_believing 1.0.1 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
data/Readme.md CHANGED
@@ -141,16 +141,37 @@ Known Issues
141
141
  ============
142
142
 
143
143
  * `BEGIN/END` breaks things and I probably won't take the time to fix it, becuase it's nontrivial and its really meant for command-line scripts, but there is currently a cuke for it
144
- * Heredocs aren't recorded. It might actually be possible if the ExpressionList were to get smarter
145
144
 
146
145
  Todo
147
146
  ====
148
147
 
149
- * Make a Lines class which is a collection of lines, responsible for managing the trailing newlines in Binary::PrintResultsNextToLines and SeeingIsBelieving/ExpressionList
150
148
  * Add examples of invocations to the help screen
151
149
  * Add xmpfilter option to sublime text
152
150
  * Update TextMate examples to use same keys as sublime, add xmpfilter option on cmd+opt+N
153
- * Move as much of the SyntaxAnalyzer as possible over to Parser and ditch Ripper altogether
151
+ * Remove the SyntaxAnalyzer altogether!
152
+ * How about if begin/rescue/end was able to record the result on the rescue section
153
+ * Check how begin/rescue/end with multiple rescue blocks works
154
+ * What about recording the result of a line inside of a string interpolation, e.g. "a#{\n1\n}b" could record line 2 is 1 and line 3 is "a\n1\nb"
155
+ * Be able to clean an invalid file (used to be able to do this, but parser can't identify comments in an invalid file the way that I'm currently using it, cuke is still there, marked as @not-implemented)
156
+ * Add a flag to allow you to just get the results so that it can be easily used without a Ruby runtime
157
+
158
+ Up Next
159
+ =======
160
+
161
+ rename ProgramRewriter -> WrapExpressions
162
+ find that fucking regex bug
163
+ add the inspected result --debug output
164
+ Fix high-level shit:
165
+ remove all previous output except if -x flag is set, then leave `# =>`
166
+ `gsub(/\s*$/, '')`
167
+ run it through sib
168
+ if -x flag is set
169
+ for each comment
170
+ if it is `# =>`, update it
171
+ else
172
+ printable_list = get a list of each line that is an actual end and has no comments with sib (can ignore heredocs b/c they will have no value)
173
+ this list is like line_number => [col_number, character_number] (e.g. where to insert on that line)
174
+ for each line, add it at that character location, adjusting from the col_number to pad it correctly
154
175
 
155
176
 
156
177
  License
@@ -29,11 +29,11 @@ Feature: Running the binary unsuccessfully
29
29
  """
30
30
  def first_defined
31
31
  second_defined
32
- end # => nil
32
+ end
33
33
 
34
34
  def second_defined
35
35
  require_relative 'raises_exception' # ~> RuntimeError: ZOMG\n!!!!
36
- end # => nil
36
+ end
37
37
 
38
38
  first_defined
39
39
 
@@ -64,14 +64,14 @@ Feature: Running the binary successfully
64
64
  And stdout is:
65
65
  """
66
66
  # iteration
67
- 5.times do |i|
67
+ 5.times do |i| # => 5
68
68
  i * 2 # => 0, 2, 4, 6, 8
69
69
  end # => 5
70
70
 
71
71
  # method and invocations
72
72
  def meth(n)
73
73
  n # => "12", "34"
74
- end # => nil
74
+ end
75
75
 
76
76
  meth "12" # => "12"
77
77
  meth "34" # => "34"
@@ -89,25 +89,25 @@ Feature: Running the binary successfully
89
89
  b/x # => /a\n b/x
90
90
 
91
91
  # don't record heredocs b/c they're just too fucking different
92
- <<HERE
92
+ <<HERE # => "is a doc\n"
93
93
  is a doc
94
94
  HERE
95
95
 
96
96
  # method invocation that occurs entirely on the next line
97
- [*1..10]
98
- .select(&:even?)
97
+ [*1..10] # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
98
+ .select(&:even?) # => [2, 4, 6, 8, 10]
99
99
  .map { |n| n * 2 } # => [4, 8, 12, 16, 20]
100
100
 
101
101
  # mutliple levels of nesting
102
102
  class User
103
103
  def initialize(name)
104
104
  @name = name # => "Josh", "Rick"
105
- end # => nil
105
+ end
106
106
 
107
107
  def name
108
108
  @name # => "Josh", "Rick"
109
- end # => nil
110
- end # => nil
109
+ end
110
+ end
111
111
 
112
112
  User.new("Josh").name # => "Josh"
113
113
  User.new("Rick").name # => "Rick"
@@ -187,7 +187,7 @@ Feature: Running the binary successfully
187
187
  __LINE__ # => 2
188
188
  $stdout.puts "omg" # => nil
189
189
  $stderr.puts "hi" # => nil
190
- DATA.read # => "1\n2"
190
+ DATA.read # => "1\n2\n"
191
191
  __LINE__ # => 6
192
192
 
193
193
  # >> omg
@@ -459,6 +459,8 @@ Feature: Using flags
459
459
  2+2 # => 10
460
460
  "a
461
461
  b" # =>
462
+ /a
463
+ b/ # =>
462
464
  1
463
465
  "omg"
464
466
  # =>
@@ -474,6 +476,8 @@ Feature: Using flags
474
476
  2+2 # => 4
475
477
  "a
476
478
  b" # => "a\n b"
479
+ /a
480
+ b/ # => /a\n b/
477
481
  1
478
482
  "omg"
479
483
  # => "omg"
@@ -481,6 +485,7 @@ Feature: Using flags
481
485
  # => "omg2"
482
486
  """
483
487
 
488
+ # TODO: do we want to print the source without its comments?
484
489
  Scenario: --debug
485
490
  Given the file "simple_program.rb":
486
491
  """
@@ -492,16 +497,13 @@ Feature: Using flags
492
497
  Then stderr is empty
493
498
  And the exit status is 0
494
499
  # source without comments
495
- And stdout includes "SOURCE WITHOUT COMMENTS:"
496
- And stdout includes:
497
- """
498
- # encoding: utf-8
499
- 1
500
- 2
501
- """
502
- # expression evaluation
503
- And stdout includes "EXPRESSION EVALUATION:"
504
- And stdout includes "GENERATED"
500
+ # And stdout includes "SOURCE WITHOUT COMMENTS:"
501
+ # And stdout includes:
502
+ # """
503
+ # # encoding: utf-8
504
+ # 1
505
+ # 2
506
+ # """
505
507
  # translated program
506
508
  And stdout includes "TRANSLATED PROGRAM:"
507
509
  And stdout includes "$seeing_is_believing_current_result"
@@ -62,10 +62,10 @@ Feature:
62
62
  Then stdout is:
63
63
  """
64
64
  def m
65
- if true
66
- return 1
65
+ if true # => true
66
+ return 1 # => 1
67
67
  end
68
- end # => nil
68
+ end
69
69
  m # => 1
70
70
  """
71
71
 
@@ -163,3 +163,17 @@ Feature:
163
163
  # encoding: utf-8
164
164
  'ç' # => "ç"
165
165
  """
166
+
167
+ @not-implemented
168
+ Scenario: Some strings look like comments
169
+ Given the file "strings_that_look_like_comments.rb":
170
+ """
171
+ "1
172
+ #{2}"
173
+ """
174
+ When I run "seeing_is_believing strings_that_look_like_comments.rb"
175
+ Then stdout is:
176
+ """
177
+ "1
178
+ #{2}" # => "1\n 2"
179
+ """
@@ -6,9 +6,10 @@ require 'seeing_is_believing/queue'
6
6
  require 'seeing_is_believing/result'
7
7
  require 'seeing_is_believing/version'
8
8
  require 'seeing_is_believing/debugger'
9
- require 'seeing_is_believing/expression_list'
10
9
  require 'seeing_is_believing/remove_inline_comments'
11
10
  require 'seeing_is_believing/evaluate_by_moving_files'
11
+ require 'seeing_is_believing/program_rewriter'
12
+ require 'seeing_is_believing/syntax_analyzer' # can we get rid of this?
12
13
 
13
14
  # might not work on windows b/c of assumptions about line ends
14
15
  class SeeingIsBelieving
@@ -20,109 +21,50 @@ class SeeingIsBelieving
20
21
  end
21
22
 
22
23
  def initialize(program, options={})
23
- program_string = RemoveInlineComments::NonLeading.call program
24
- @stream = to_stream program_string
24
+ @program = program
25
25
  @matrix_filename = options[:matrix_filename]
26
26
  @filename = options[:filename]
27
27
  @stdin = to_stream options.fetch(:stdin, '')
28
28
  @require = options.fetch :require, []
29
29
  @load_path = options.fetch :load_path, []
30
30
  @encoding = options.fetch :encoding, nil
31
- @line_number = 1
32
31
  @timeout = options[:timeout]
33
32
  @debugger = options.fetch :debugger, Debugger.new(enabled: false)
34
-
35
- debugger.context("SOURCE WITHOUT COMMENTS") { program_string }
36
33
  end
37
34
 
38
- # I'd like to refactor this, but I was unsatisfied with the three different things I tried.
39
- # In the end, I prefer keeping all manipulation of the line number here in the main function
40
- # And I like that the higher-level construct of how the program gets built can be found here.
41
35
  def call
42
36
  @memoized_result ||= begin
43
- leading_comments = ''
44
-
45
-
46
- # extract leading comments (e.g. encoding) so they don't get wrapped in begin/rescue/end
47
- while SyntaxAnalyzer.line_is_comment?(next_line_queue.peek)
48
- leading_comments << next_line_queue.dequeue << "\n"
49
- @line_number += 1
50
- end
51
-
52
- # extract leading =begin/=end so they don't get wrapped in begin/rescue/end
53
- while SyntaxAnalyzer.begins_multiline_comment?(next_line_queue.peek)
54
- lines = next_line_queue.dequeue << "\n"
55
- @line_number += 1
56
- until SyntaxAnalyzer.begin_and_end_comments_are_complete? lines
57
- lines << next_line_queue.dequeue << "\n"
58
- @line_number += 1
59
- end
60
- leading_comments << lines
61
- end
62
-
63
- # extract program body
64
- body = ''
65
- until next_line_queue.empty? || data_segment?
66
- expression, expression_size = expression_list.call
67
- body << expression
68
- track_line_number @line_number
69
- @line_number += expression_size
70
- end
71
-
72
- # extract data segment
73
- data_segment = ''
74
- data_segment = "\n#{the_rest_of_the_stream}" if data_segment?
75
-
76
- # build the program
77
- program = leading_comments << record_exceptions_in(body) << data_segment
78
- debugger.context("TRANSLATED PROGRAM") { program }
79
-
80
- # return the result
81
- result_for program, max_line_number
37
+ # must use newline after code, or comments will comment out rescue section
38
+ # FIXME: IS THIS STILL TRUE?
39
+ wrapped = ProgramReWriter.call "#@program\n",
40
+ before_all: "begin;",
41
+ after_all: "\n"\
42
+ "rescue Exception;"\
43
+ "line_number = $!.backtrace.grep(/\#{__FILE__}/).first[/:\\d+/][1..-1].to_i;"\
44
+ "$seeing_is_believing_current_result.record_exception line_number, $!;"\
45
+ "$seeing_is_believing_current_result.exitstatus = 1;"\
46
+ "$seeing_is_believing_current_result.exitstatus = $!.status if $!.kind_of? SystemExit;"\
47
+ "end",
48
+ before_each: -> line_number { "($seeing_is_believing_current_result.record_result(#{line_number}, (" },
49
+ after_each: -> line_number { ")))" }
50
+ debugger.context("TRANSLATED PROGRAM") { wrapped }
51
+ result = result_for wrapped
52
+ debugger.context("RESULT") { result.inspect }
53
+ result
82
54
  end
83
55
  end
84
56
 
85
- private
86
-
87
- attr_reader :stream, :matrix_filename, :debugger
88
57
 
89
- def expression_list
90
- @expression_list ||= ExpressionList.new debugger: debugger,
91
- get_next_line: lambda { next_line_queue.dequeue },
92
- peek_next_line: lambda { next_line_queue.peek },
93
- on_complete: lambda { |line, children, completions, offset|
94
- expression = [line, *children, *completions].map(&:chomp).join("\n")
58
+ private
95
59
 
96
- if do_not_record? expression
97
- expression + "\n"
98
- else
99
- record_yahself(expression, @line_number+offset) + "\n"
100
- end
101
- }
102
- end
60
+ attr_reader :matrix_filename, :debugger
103
61
 
104
62
  def to_stream(string_or_stream)
105
63
  return string_or_stream if string_or_stream.respond_to? :gets
106
64
  StringIO.new string_or_stream
107
65
  end
108
66
 
109
- def record_yahself(line, line_number)
110
- "($seeing_is_believing_current_result.record_result(#{line_number}, (#{line})))"
111
- end
112
-
113
- def record_exceptions_in(code)
114
- # must use newline after code, or comments will comment out rescue section
115
- "begin;"\
116
- "#{code}\n"\
117
- "rescue Exception;"\
118
- "line_number = $!.backtrace.grep(/\#{__FILE__}/).first[/:\\d+/][1..-1].to_i;"\
119
- "$seeing_is_believing_current_result.record_exception line_number, $!;"\
120
- "$seeing_is_believing_current_result.exitstatus = 1;"\
121
- "$seeing_is_believing_current_result.exitstatus = $!.status if $!.kind_of? SystemExit;"\
122
- "end"
123
- end
124
-
125
- def result_for(program, max_line_number)
67
+ def result_for(program)
126
68
  Dir.mktmpdir "seeing_is_believing_temp_dir" do |dir|
127
69
  filename = @filename || File.join(dir, 'program.rb')
128
70
  EvaluateByMovingFiles.new(program,
@@ -134,31 +76,6 @@ class SeeingIsBelieving
134
76
  encoding: @encoding,
135
77
  timeout: @timeout)
136
78
  .call
137
- .track_line_number(max_line_number)
138
79
  end
139
80
  end
140
-
141
- def eof?
142
- next_line_queue.peek.nil?
143
- end
144
-
145
- def data_segment?
146
- SyntaxAnalyzer.begins_data_segment?(next_line_queue.peek)
147
- end
148
-
149
- def next_line_queue
150
- @next_line_queue ||= Queue.new { (line = stream.gets) && line.chomp }
151
- end
152
-
153
- # eh? -.^ (can't we pull the rest of this out of the queue instead of breaking encapsulation?)
154
- def the_rest_of_the_stream
155
- next_line_queue.dequeue << "\n" << stream.read
156
- end
157
-
158
- def do_not_record?(code)
159
- code =~ BLANK_REGEX ||
160
- SyntaxAnalyzer.ends_in_comment?(code) ||
161
- SyntaxAnalyzer.void_value_expression?(code) ||
162
- SyntaxAnalyzer.here_doc?(code)
163
- end
164
81
  end
@@ -89,9 +89,9 @@ class SeeingIsBelieving
89
89
  def add_line(line, line_number)
90
90
  should_record = should_record? line, line_number
91
91
  if should_record && xmpfilter_style && line.strip =~ /^# =>/
92
- new_body << xmpfilter_update(line, file_result[line_number - 1])
92
+ new_body << xmpfilter_update(line, file_result[line_number - 1]) # There is probably a bug in this since it doesn't go through the LineFormatter it can probably be to long
93
93
  elsif should_record && xmpfilter_style
94
- new_body << xmpfilter_update(line, file_result[line_number])
94
+ new_body << xmpfilter_update(line, file_result[line_number]) # There is probably a bug in this since it doesn't go through the LineFormatter it can probably be to long
95
95
  elsif should_record
96
96
  new_body << format_line(line.chomp, line_number, file_result[line_number])
97
97
  else
@@ -108,7 +108,7 @@ class SeeingIsBelieving
108
108
 
109
109
  # again, this is too naive, should actually parse the comments and update them
110
110
  def xmpfilter_update(line, line_results)
111
- line.gsub /# =>.*?$/, "# => #{line_results.join ', '}"
111
+ line.gsub /# =>.*?$/, "# => #{line_results.join(', ').gsub("\n", '\n')}"
112
112
  end
113
113
 
114
114
  def add_stdout
@@ -0,0 +1,280 @@
1
+ require 'parser/current'
2
+
3
+ # hack rewriter to apply insertions in stable order
4
+ # until https://github.com/whitequark/parser/pull/102 gets merged in
5
+ module Parser
6
+ module Source
7
+ class Rewriter
8
+ def process
9
+ adjustment = 0
10
+ source = @source_buffer.source.dup
11
+ sorted_queue = @queue.sort_by.with_index do |action, index|
12
+ [action.range.begin_pos, index]
13
+ end
14
+ sorted_queue.each do |action|
15
+ begin_pos = action.range.begin_pos + adjustment
16
+ end_pos = begin_pos + action.range.length
17
+
18
+ source[begin_pos...end_pos] = action.replacement
19
+
20
+ adjustment += (action.replacement.length - action.range.length)
21
+ end
22
+
23
+ source
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ # comprehensive list of syntaxes that can come up
31
+ # https://github.com/whitequark/parser/blob/master/doc/AST_FORMAT.md
32
+ class SeeingIsBelieving
33
+ class ProgramReWriter
34
+ def self.call(program, wrappings)
35
+ new(program, wrappings).call
36
+ end
37
+
38
+ def initialize(program, wrappings)
39
+ self.program = program
40
+ self.before_all = wrappings.fetch :before_all, ''.freeze
41
+ self.after_all = wrappings.fetch :after_all, ''.freeze
42
+ self.before_each = wrappings.fetch :before_each, -> * { '' }
43
+ self.after_each = wrappings.fetch :after_each, -> * { '' }
44
+ self.buffer = Parser::Source::Buffer.new('program-without-annotations')
45
+ buffer.source = program
46
+ self.root = Parser::CurrentRuby.new.parse buffer
47
+ self.rewriter = Parser::Source::Rewriter.new buffer
48
+ self.wrappings = {}
49
+ rescue Parser::SyntaxError => e
50
+ raise ::SyntaxError, e.message
51
+ end
52
+
53
+ def call
54
+ @called ||= begin
55
+ find_wrappings
56
+
57
+ if root # file may be empty
58
+ rewriter.insert_before root.location.expression, before_all
59
+
60
+ wrappings.each do |line_num, (range, last_col)|
61
+ rewriter.insert_before range, before_each.call(line_num)
62
+ rewriter.insert_after range, after_each.call(line_num)
63
+ end
64
+
65
+ rewriter.insert_after root.location.expression, after_all
66
+ end
67
+
68
+ rewriter.process
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ attr_accessor :program, :before_all, :after_all, :before_each, :after_each, :buffer, :root, :rewriter, :wrappings
75
+
76
+ def add_to_wrappings(range_or_ast)
77
+ range = range_or_ast
78
+ range = range_or_ast.location.expression if range.kind_of? ::AST::Node
79
+ line, col = buffer.decompose_position range.end_pos
80
+ _, prev_col = wrappings[line]
81
+ wrappings[line] = (!wrappings[line] || prev_col < col ? [range, col] : wrappings[line] )
82
+ end
83
+
84
+ def add_children(ast, omit_first = false)
85
+ (omit_first ? ast.children.drop(1) : ast.children)
86
+ .each { |child| find_wrappings child }
87
+ end
88
+
89
+ def find_wrappings(ast=root)
90
+ return wrappings unless ast.kind_of? ::AST::Node
91
+
92
+ case ast.type
93
+ when :args, :redo, :retry, :alias, :undef, :splat, :match_current_line
94
+ # no op
95
+ when :rescue, :ensure, :def, :return, :break, :next
96
+ add_children ast
97
+ when :if
98
+ if ast.location.kind_of? Parser::Source::Map::Ternary
99
+ add_to_wrappings ast unless ast.children.any? { |child| void_value? child }
100
+ add_children ast
101
+ else
102
+ keyword = ast.location.keyword.source
103
+ if (keyword == 'if' || keyword == 'unless') && ast.children.none? { |child| void_value? child }
104
+ add_to_wrappings ast
105
+ end
106
+ add_children ast
107
+ end
108
+ when :when, :pair, :defs, :class, :module, :sclass
109
+ find_wrappings ast.children.last
110
+ when :resbody
111
+ exception_type, variable_name, body = ast.children
112
+ find_wrappings body
113
+ when :block
114
+ add_to_wrappings ast
115
+
116
+ # a {} comes in as
117
+ # (block
118
+ # (send nil :a)
119
+ # (args) nil)
120
+ #
121
+ # a.b {} comes in as
122
+ # (block
123
+ # (send
124
+ # (send nil :a) :b)
125
+ # (args) nil)
126
+ #
127
+ # we don't want to wrap the send itself, otherwise could come in as <a>{}
128
+ # but we do want ot wrap its first child so that we can get <<a>\n.b{}>
129
+ #
130
+ # I can't think of anything other than a :send that could be the first child
131
+ # but I'll check for it anyway.
132
+ the_send = ast.children[0]
133
+ find_wrappings the_send.children.first if the_send.type == :send
134
+ add_children ast, true
135
+ when :masgn
136
+ # we must look at RHS because [1,<<A] and 1,<<A are both allowed
137
+ #
138
+ # in the first case, we must take the end_pos of the array, or we'll insert the after_each in the wrong location
139
+ #
140
+ # in the second, there is an implicit Array wrapped around it, with the wrong end_pos,
141
+ # so we must take the end_pos of the last arg
142
+ array = ast.children.last
143
+ if array.location.expression.source.start_with? '['
144
+ add_to_wrappings ast
145
+ find_wrappings array
146
+ else
147
+ begin_pos = ast.location.expression.begin_pos
148
+ end_pos = heredoc_hack(ast.children.last.children.last).location.expression.end_pos
149
+ range = Parser::Source::Range.new buffer, begin_pos, end_pos
150
+ add_to_wrappings range
151
+ add_children ast.children.last
152
+ end
153
+ when :lvasgn
154
+ # because the RHS can be a heredoc, and parser currently handles heredocs locations incorrectly
155
+ # we must hack around this
156
+
157
+ # can have one or two children:
158
+ # in a=1 (has children :a, and 1)
159
+ # in a,b=1,2 it comes out like:
160
+ # (masgn
161
+ # (mlhs
162
+ # (lvasgn :a) <-- one child
163
+ #
164
+ # (lvasgn :b))
165
+ # (array
166
+ # (int 1)
167
+ # (int 2)))
168
+ if ast.children.size == 2
169
+ begin_pos = ast.location.expression.begin_pos
170
+ end_pos = heredoc_hack(ast.children.last).location.expression.end_pos
171
+ range = Parser::Source::Range.new buffer, begin_pos, end_pos
172
+ add_to_wrappings range
173
+ add_children ast
174
+ end
175
+ when :send
176
+ # because the target and the last child can be heredocs
177
+ # and the method may or may not have parens,
178
+ # it can inadvertently inherit the incorrect location of the heredocs
179
+ # so we check for this case, that way we can construct the correct range instead
180
+ range = ast.location.expression
181
+
182
+ # first two children: target, message, so we want the last child only if it is an argument
183
+ target, message, *, last_arg = ast.children
184
+
185
+ # last arg is a heredoc, use the closing paren, or the end of the first line of the heredoc
186
+ if heredoc? last_arg
187
+ end_pos = heredoc_hack(last_arg).location.expression.end_pos
188
+ if buffer.source[ast.location.selector.end_pos] == '('
189
+ end_pos += 1 until buffer.source[end_pos] == ')'
190
+ end_pos += 1
191
+ end
192
+
193
+ # the last arg is not a heredoc, the range of the expression can be trusted
194
+ elsif last_arg
195
+ end_pos = ast.location.expression.end_pos
196
+
197
+ # there is no last arg, but there are parens, find the closing paren
198
+ # we can't trust the expression range because the *target* could be a heredoc
199
+ # FIXME: This blows up on 2.0 with ->{}.() because it has no selector, so in this case
200
+ # we would want to use the expression, but I'm ignoring that for now, because
201
+ # we would have to check the target to see whether to use the selector or the expression
202
+ elsif buffer.source[ast.location.selector.end_pos] == '('
203
+ closing_paren_index = ast.location.selector.end_pos + 1
204
+ closing_paren_index += 1 until buffer.source[closing_paren_index] == ')'
205
+ end_pos = closing_paren_index + 1
206
+
207
+ # use the selector because we can't trust expression since target can be a heredoc
208
+ elsif heredoc? target
209
+ end_pos = ast.location.selector.end_pos
210
+
211
+ # use the expression because it could be something like !1, in which case the selector would return the rhs of the !
212
+ else
213
+ end_pos = ast.location.expression.end_pos
214
+ end
215
+
216
+ begin_pos = ast.location.expression.begin_pos
217
+ range = Parser::Source::Range.new(buffer, begin_pos, end_pos)
218
+ add_to_wrappings range
219
+ add_children ast
220
+ when :begin
221
+ last_child = ast.children.last
222
+ if heredoc? last_child
223
+ range = Parser::Source::Range.new buffer,
224
+ ast.location.expression.begin_pos,
225
+ heredoc_hack(last_child).location.expression.end_pos
226
+ add_to_wrappings range unless void_value? ast.children.last
227
+ end
228
+
229
+ add_children ast
230
+ when :str, :dstr, :xstr, :regexp
231
+ add_to_wrappings heredoc_hack ast
232
+ else
233
+ add_to_wrappings ast
234
+ add_children ast
235
+ end
236
+ rescue
237
+ # TODO: delete this rescue block once things get stabler
238
+ puts ast.type
239
+ puts $!
240
+ require "pry"
241
+ binding.pry
242
+ end
243
+
244
+ def heredoc_hack(ast)
245
+ return ast unless heredoc? ast
246
+ Parser::AST::Node.new :str,
247
+ [],
248
+ location: Parser::Source::Map.new(ast.location.begin)
249
+ end
250
+
251
+ # this is the scardest fucking method I think I've ever written.
252
+ # *anything* can go wrong!
253
+ def heredoc?(ast)
254
+ # some strings are fucking weird.
255
+ # e.g. the "1" in `%w[1]` returns nil for ast.location.begin
256
+ # and `__FILE__` is a string whose location is a Parser::Source::Map instead of a Parser::Source::Map::Collection, so it has no #begin
257
+ ast.kind_of?(Parser::AST::Node) &&
258
+ (ast.type == :dstr || ast.type == :str) &&
259
+ (location = ast.location) &&
260
+ (location.respond_to?(:begin)) &&
261
+ (the_begin = location.begin) &&
262
+ (the_begin.source =~ /^\<\<-?/)
263
+ end
264
+
265
+ def void_value?(ast)
266
+ case ast && ast.type
267
+ when :begin, :kwbegin, :resbody
268
+ void_value?(ast.children[-1])
269
+ when :rescue, :ensure
270
+ ast.children.any? { |child| void_value? child }
271
+ when :if
272
+ void_value?(ast.children[1]) || void_value?(ast.children[2])
273
+ when :return, :next, :redo, :retry, :break
274
+ true
275
+ else
276
+ false
277
+ end
278
+ end
279
+ end
280
+ end