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 +24 -3
- data/features/errors.feature +2 -2
- data/features/examples.feature +9 -9
- data/features/flags.feature +12 -10
- data/features/regression.feature +17 -3
- data/lib/seeing_is_believing.rb +23 -106
- data/lib/seeing_is_believing/binary/add_annotations.rb +3 -3
- data/lib/seeing_is_believing/program_rewriter.rb +280 -0
- data/lib/seeing_is_believing/remove_inline_comments.rb +1 -1
- data/lib/seeing_is_believing/result.rb +15 -5
- data/lib/seeing_is_believing/syntax_analyzer.rb +0 -155
- data/lib/seeing_is_believing/version.rb +1 -1
- data/seeing_is_believing.gemspec +1 -1
- data/spec/program_rewriter_spec.rb +687 -0
- data/spec/seeing_is_believing_spec.rb +66 -114
- data/spec/syntax_analyzer_spec.rb +3 -316
- metadata +10 -10
- data/lib/seeing_is_believing/expression_list.rb +0 -101
- data/spec/expression_list_spec.rb +0 -277
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
|
-
*
|
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
|
data/features/errors.feature
CHANGED
@@ -29,11 +29,11 @@ Feature: Running the binary unsuccessfully
|
|
29
29
|
"""
|
30
30
|
def first_defined
|
31
31
|
second_defined
|
32
|
-
end
|
32
|
+
end
|
33
33
|
|
34
34
|
def second_defined
|
35
35
|
require_relative 'raises_exception' # ~> RuntimeError: ZOMG\n!!!!
|
36
|
-
end
|
36
|
+
end
|
37
37
|
|
38
38
|
first_defined
|
39
39
|
|
data/features/examples.feature
CHANGED
@@ -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
|
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
|
105
|
+
end
|
106
106
|
|
107
107
|
def name
|
108
108
|
@name # => "Josh", "Rick"
|
109
|
-
end
|
110
|
-
end
|
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
|
data/features/flags.feature
CHANGED
@@ -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"
|
data/features/regression.feature
CHANGED
@@ -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
|
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
|
+
"""
|
data/lib/seeing_is_believing.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|