power_assert 0.4.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/BSDL +1 -1
- data/COPYING +1 -1
- data/README.rdoc +1 -0
- data/Rakefile +12 -4
- data/bin/console +12 -0
- data/bin/setup +6 -0
- data/lib/power_assert.rb +43 -288
- data/lib/power_assert/colorize.rb +9 -0
- data/lib/power_assert/configuration.rb +25 -3
- data/lib/power_assert/context.rb +201 -0
- data/lib/power_assert/enable_tracepoint_events.rb +6 -1
- data/lib/power_assert/inspector.rb +61 -0
- data/lib/power_assert/parser.rb +239 -0
- data/lib/power_assert/version.rb +1 -1
- data/power_assert.gemspec +12 -5
- metadata +52 -8
- data/benchmarks/bm_yhpg.rb +0 -59
- data/benchmarks/helper.rb +0 -8
- data/test/helper.rb +0 -10
- data/test/test_power_assert.rb +0 -514
@@ -1,7 +1,7 @@
|
|
1
1
|
module PowerAssert
|
2
2
|
class << self
|
3
3
|
def configuration
|
4
|
-
@configuration ||= Configuration[false, false, true]
|
4
|
+
@configuration ||= Configuration[false, false, true, false, false]
|
5
5
|
end
|
6
6
|
|
7
7
|
def configure
|
@@ -12,13 +12,35 @@ module PowerAssert
|
|
12
12
|
SUPPORT_ALIAS_METHOD = TracePoint.public_method_defined?(:callee_id)
|
13
13
|
private_constant :SUPPORT_ALIAS_METHOD
|
14
14
|
|
15
|
-
class Configuration < Struct.new(:lazy_inspection, :_trace_alias_method, :_redefinition)
|
15
|
+
class Configuration < Struct.new(:lazy_inspection, :_trace_alias_method, :_redefinition, :_colorize_message, :_use_pp)
|
16
16
|
def _trace_alias_method=(bool)
|
17
17
|
super
|
18
18
|
if SUPPORT_ALIAS_METHOD
|
19
|
-
warn '_trace_alias_method option is obsolete. You no longer have to set it.'
|
19
|
+
warn 'power_assert: _trace_alias_method option is obsolete. You no longer have to set it.'
|
20
20
|
end
|
21
21
|
end
|
22
|
+
|
23
|
+
def _colorize_message=(bool)
|
24
|
+
if bool
|
25
|
+
require 'pry'
|
26
|
+
end
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def lazy_inspection=(bool)
|
31
|
+
unless bool
|
32
|
+
raise 'lazy_inspection option must be enabled when using pp' if _use_pp
|
33
|
+
end
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def _use_pp=(bool)
|
38
|
+
if bool
|
39
|
+
raise 'lazy_inspection option must be enabled when using pp' unless lazy_inspection
|
40
|
+
require 'pp'
|
41
|
+
end
|
42
|
+
super
|
43
|
+
end
|
22
44
|
end
|
23
45
|
private_constant :Configuration
|
24
46
|
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'power_assert/configuration'
|
2
|
+
require 'power_assert/enable_tracepoint_events'
|
3
|
+
require 'power_assert/inspector'
|
4
|
+
require 'power_assert/parser'
|
5
|
+
|
6
|
+
module PowerAssert
|
7
|
+
class Context
|
8
|
+
Value = Struct.new(:name, :value, :column)
|
9
|
+
|
10
|
+
attr_reader :message_proc
|
11
|
+
|
12
|
+
def initialize(base_caller_length)
|
13
|
+
@fired = false
|
14
|
+
@target_thread = Thread.current
|
15
|
+
method_id_set = nil
|
16
|
+
return_values = []
|
17
|
+
trace_alias_method = PowerAssert.configuration._trace_alias_method
|
18
|
+
@trace_return = TracePoint.new(:return, :c_return) do |tp|
|
19
|
+
method_id_set ||= @parser.method_id_set
|
20
|
+
method_id = SUPPORT_ALIAS_METHOD ? tp.callee_id :
|
21
|
+
trace_alias_method && tp.event == :return ? tp.binding.eval('::Kernel.__callee__') :
|
22
|
+
tp.method_id
|
23
|
+
next if ! method_id_set[method_id]
|
24
|
+
next if tp.event == :c_return and
|
25
|
+
not (@parser.lineno == tp.lineno and @parser.path == tp.path)
|
26
|
+
next unless tp.binding # workaround for ruby 2.2
|
27
|
+
locs = PowerAssert.app_caller_locations
|
28
|
+
diff = locs.length - base_caller_length
|
29
|
+
if (tp.event == :c_return && diff == 1 || tp.event == :return && diff <= 2) and Thread.current == @target_thread
|
30
|
+
idx = -(base_caller_length + 1)
|
31
|
+
if @parser.path == locs[idx].path and @parser.lineno == locs[idx].lineno
|
32
|
+
val = PowerAssert.configuration.lazy_inspection ?
|
33
|
+
tp.return_value :
|
34
|
+
InspectedValue.new(SafeInspectable.new(tp.return_value).inspect)
|
35
|
+
return_values << Value[method_id.to_s, val, nil]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@message_proc = -> {
|
40
|
+
raise RuntimeError, 'call #yield or #enable at first' unless fired?
|
41
|
+
@message ||= build_assertion_message(@parser.line, @parser.idents, @parser.binding, return_values).freeze
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def message
|
46
|
+
@message_proc.()
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def fired?
|
52
|
+
@fired
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_assertion_message(line, idents, proc_binding, return_values)
|
56
|
+
if PowerAssert.configuration._colorize_message
|
57
|
+
line = Pry::Code.new(line).highlighted
|
58
|
+
end
|
59
|
+
|
60
|
+
path = detect_path(idents, return_values)
|
61
|
+
return line unless path
|
62
|
+
|
63
|
+
delete_unidentified_calls(return_values, path)
|
64
|
+
methods, refs = path.partition {|i| i.type == :method }
|
65
|
+
return_values.zip(methods) do |i, j|
|
66
|
+
unless i.name == j.name
|
67
|
+
warn "power_assert: [BUG] Failed to get column: #{i.name}"
|
68
|
+
return line
|
69
|
+
end
|
70
|
+
i.column = j.column
|
71
|
+
end
|
72
|
+
ref_values = refs.map {|i| Value[i.name, proc_binding.eval(i.name), i.column] }
|
73
|
+
vals = (return_values + ref_values).find_all(&:column).sort_by(&:column).reverse
|
74
|
+
return line if vals.empty?
|
75
|
+
|
76
|
+
fmt = (0..vals[0].column).map {|i| vals.find {|v| v.column == i } ? "%<#{i}>s" : ' ' }.join
|
77
|
+
lines = []
|
78
|
+
lines << line.chomp
|
79
|
+
lines << sprintf(fmt, vals.each_with_object({}) {|v, h| h[v.column.to_s.to_sym] = '|' }).chomp
|
80
|
+
vals.each do |i|
|
81
|
+
inspected_val = SafeInspectable.new(Formatter.new(i.value, i.column)).inspect
|
82
|
+
inspected_val.each_line do |l|
|
83
|
+
map_to = vals.each_with_object({}) do |j, h|
|
84
|
+
h[j.column.to_s.to_sym] = [l, '|', ' '][i.column <=> j.column]
|
85
|
+
end
|
86
|
+
lines << encoding_safe_rstrip(sprintf(fmt, map_to))
|
87
|
+
end
|
88
|
+
end
|
89
|
+
lines.join("\n")
|
90
|
+
end
|
91
|
+
|
92
|
+
def detect_path(idents, return_values)
|
93
|
+
return @parser.call_paths.flatten.uniq if @parser.method_id_set.empty?
|
94
|
+
all_paths = @parser.call_paths
|
95
|
+
return_value_names = return_values.map(&:name)
|
96
|
+
uniq_calls = uniq_calls(all_paths)
|
97
|
+
uniq_call = return_value_names.find {|i| uniq_calls.include?(i) }
|
98
|
+
detected_paths = all_paths.find_all do |path|
|
99
|
+
method_names = path.find_all {|ident| ident.type == :method }.map(&:name)
|
100
|
+
break [path] if uniq_call and method_names.include?(uniq_call)
|
101
|
+
return_value_names == method_names
|
102
|
+
end
|
103
|
+
return nil unless detected_paths.length == 1
|
104
|
+
detected_paths[0]
|
105
|
+
end
|
106
|
+
|
107
|
+
def uniq_calls(paths)
|
108
|
+
all_calls = enum_count_by(paths.map {|path| path.find_all {|ident| ident.type == :method }.map(&:name).uniq }.flatten) {|i| i }
|
109
|
+
all_calls.find_all {|_, call_count| call_count == 1 }.map {|name, _| name }
|
110
|
+
end
|
111
|
+
|
112
|
+
def delete_unidentified_calls(return_values, path)
|
113
|
+
return_value_num_of_calls = enum_count_by(return_values, &:name)
|
114
|
+
path_num_of_calls = enum_count_by(path.find_all {|ident| ident.type == :method }, &:name)
|
115
|
+
identified_calls = return_value_num_of_calls.find_all {|name, num| path_num_of_calls[name] == num }.map(&:first)
|
116
|
+
return_values.delete_if {|val| ! identified_calls.include?(val.name) }
|
117
|
+
path.delete_if {|ident| ident.type == :method and ! identified_calls.include?(ident.name) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def enum_count_by(enum, &blk)
|
121
|
+
Hash[enum.group_by(&blk).map{|k, v| [k, v.length] }]
|
122
|
+
end
|
123
|
+
|
124
|
+
def encoding_safe_rstrip(str)
|
125
|
+
str.rstrip
|
126
|
+
rescue ArgumentError, Encoding::CompatibilityError
|
127
|
+
enc = str.encoding
|
128
|
+
if enc.ascii_compatible?
|
129
|
+
str.b.rstrip.force_encoding(enc)
|
130
|
+
else
|
131
|
+
str
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
private_constant :Context
|
136
|
+
|
137
|
+
class BlockContext < Context
|
138
|
+
def initialize(assertion_proc_or_source, assertion_method, source_binding)
|
139
|
+
super(0)
|
140
|
+
if assertion_proc_or_source.respond_to?(:to_proc)
|
141
|
+
@assertion_proc = assertion_proc_or_source.to_proc
|
142
|
+
line = nil
|
143
|
+
else
|
144
|
+
@assertion_proc = source_binding.eval "Proc.new {#{assertion_proc_or_source}}"
|
145
|
+
line = assertion_proc_or_source
|
146
|
+
end
|
147
|
+
@parser = Parser::DUMMY
|
148
|
+
@trace_call = TracePoint.new(:call, :c_call) do |tp|
|
149
|
+
if PowerAssert.app_context? and Thread.current == @target_thread
|
150
|
+
@trace_call.disable
|
151
|
+
locs = PowerAssert.app_caller_locations
|
152
|
+
path = locs.last.path
|
153
|
+
lineno = locs.last.lineno
|
154
|
+
line ||= open(path).each_line.drop(lineno - 1).first
|
155
|
+
@parser = Parser.new(line, path, lineno, @assertion_proc.binding, assertion_method.to_s)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def yield
|
161
|
+
@fired = true
|
162
|
+
invoke_yield(&@assertion_proc)
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def invoke_yield
|
168
|
+
@trace_return.enable do
|
169
|
+
@trace_call.enable do
|
170
|
+
yield
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
private_constant :BlockContext
|
176
|
+
|
177
|
+
class TraceContext < Context
|
178
|
+
def initialize(binding)
|
179
|
+
target_frame, *base = PowerAssert.app_caller_locations
|
180
|
+
super(base.length)
|
181
|
+
path = target_frame.path
|
182
|
+
lineno = target_frame.lineno
|
183
|
+
line = open(path).each_line.drop(lineno - 1).first
|
184
|
+
@parser = Parser.new(line, path, lineno, binding)
|
185
|
+
end
|
186
|
+
|
187
|
+
def enable
|
188
|
+
@fired = true
|
189
|
+
@trace_return.enable
|
190
|
+
end
|
191
|
+
|
192
|
+
def disable
|
193
|
+
@trace_return.disable
|
194
|
+
end
|
195
|
+
|
196
|
+
def enabled?
|
197
|
+
@trace_return.enabled?
|
198
|
+
end
|
199
|
+
end
|
200
|
+
private_constant :TraceContext
|
201
|
+
end
|
@@ -1,7 +1,12 @@
|
|
1
1
|
require 'power_assert/configuration'
|
2
2
|
|
3
|
-
if defined?
|
3
|
+
if defined?(RubyVM)
|
4
4
|
if PowerAssert.configuration._redefinition
|
5
|
+
if RUBY_VERSION == '2.3.2'
|
6
|
+
warn 'power_assert: It is strongly recommended that you use Ruby 2.3.3 or later which fixes regression on 2.3.2.'
|
7
|
+
warn 'power_assert: See https://www.ruby-lang.org/en/news/2016/11/21/ruby-2-3-3-released/ for more details.'
|
8
|
+
end
|
9
|
+
|
5
10
|
verbose = $VERBOSE
|
6
11
|
begin
|
7
12
|
$VERBOSE = nil
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'power_assert/configuration'
|
2
|
+
|
3
|
+
module PowerAssert
|
4
|
+
class InspectedValue
|
5
|
+
def initialize(value)
|
6
|
+
@value = value
|
7
|
+
end
|
8
|
+
|
9
|
+
def inspect
|
10
|
+
@value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
private_constant :InspectedValue
|
14
|
+
|
15
|
+
class SafeInspectable
|
16
|
+
def initialize(value)
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
inspected = @value.inspect
|
22
|
+
if Encoding.compatible?(Encoding.default_external, inspected)
|
23
|
+
inspected
|
24
|
+
else
|
25
|
+
begin
|
26
|
+
"#{inspected.encode(Encoding.default_external)}(#{inspected.encoding})"
|
27
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
28
|
+
inspected.force_encoding(Encoding.default_external)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
rescue => e
|
32
|
+
"InspectionFailure: #{e.class}: #{e.message.each_line.first}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
private_constant :SafeInspectable
|
36
|
+
|
37
|
+
class Formatter
|
38
|
+
def initialize(value, indent)
|
39
|
+
@value = value
|
40
|
+
@indent = indent
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
if PowerAssert.configuration._colorize_message
|
45
|
+
if PowerAssert.configuration._use_pp
|
46
|
+
width = [Pry::Terminal.width! - 1 - @indent, 10].max
|
47
|
+
Pry::ColorPrinter.pp(@value, '', width)
|
48
|
+
else
|
49
|
+
Pry::Code.new(@value.inspect).highlighted
|
50
|
+
end
|
51
|
+
else
|
52
|
+
if PowerAssert.configuration._use_pp
|
53
|
+
PP.pp(@value, '')
|
54
|
+
else
|
55
|
+
@value.inspect
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
private_constant :Formatter
|
61
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'ripper'
|
2
|
+
|
3
|
+
module PowerAssert
|
4
|
+
class Parser
|
5
|
+
Ident = Struct.new(:type, :name, :column)
|
6
|
+
|
7
|
+
attr_reader :line, :path, :lineno, :binding
|
8
|
+
|
9
|
+
def initialize(line, path, lineno, binding, assertion_method_name = nil)
|
10
|
+
@line = line
|
11
|
+
@line_for_parsing = valid_syntax?(line) ? line : slice_expression(line)
|
12
|
+
@path = path
|
13
|
+
@lineno = lineno
|
14
|
+
@binding = binding
|
15
|
+
@proc_local_variables = binding.eval('local_variables').map(&:to_s)
|
16
|
+
@assertion_method_name = assertion_method_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def idents
|
20
|
+
@idents ||= extract_idents(Ripper.sexp(@line_for_parsing))
|
21
|
+
end
|
22
|
+
|
23
|
+
def call_paths
|
24
|
+
collect_paths(idents).uniq
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_id_set
|
28
|
+
methods = idents.flatten.find_all {|i| i.type == :method }
|
29
|
+
@method_id_set ||= methods.map(&:name).map(&:to_sym).each_with_object({}) {|i, h| h[i] = true }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def valid_syntax?(str)
|
35
|
+
return true unless defined?(RubyVM)
|
36
|
+
begin
|
37
|
+
verbose, $VERBOSE = $VERBOSE, nil
|
38
|
+
RubyVM::InstructionSequence.compile(str)
|
39
|
+
true
|
40
|
+
rescue SyntaxError
|
41
|
+
false
|
42
|
+
ensure
|
43
|
+
$VERBOSE = verbose
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def slice_expression(str)
|
48
|
+
str = str.chomp
|
49
|
+
str.sub!(/\A\s*(?:if|unless|elsif|case|while|until) /) {|i| ' ' * i.length }
|
50
|
+
str.sub!(/\A\s*(?:\}|\]|end)?\./) {|i| ' ' * i.length }
|
51
|
+
str.sub!(/[\{\.\\]\z/, '')
|
52
|
+
str.sub!(/(?:&&|\|\|)\z/, '')
|
53
|
+
str.sub!(/ (?:do|and|or)\z/, '')
|
54
|
+
str
|
55
|
+
end
|
56
|
+
|
57
|
+
class Branch < Array
|
58
|
+
end
|
59
|
+
|
60
|
+
AND_OR_OPS = %i(and or && ||)
|
61
|
+
|
62
|
+
#
|
63
|
+
# Returns idents as graph structure.
|
64
|
+
#
|
65
|
+
# +--c--b--+
|
66
|
+
# extract_idents(Ripper.sexp('a&.b(c).d')) #=> a--+ +--d
|
67
|
+
# +--------+
|
68
|
+
#
|
69
|
+
def extract_idents(sexp)
|
70
|
+
tag, * = sexp
|
71
|
+
case tag
|
72
|
+
when :arg_paren, :assoc_splat, :fcall, :hash, :method_add_block, :string_literal, :return
|
73
|
+
extract_idents(sexp[1])
|
74
|
+
when :assign, :massign
|
75
|
+
extract_idents(sexp[2])
|
76
|
+
when :opassign
|
77
|
+
_, _, (_, op_name, (_, op_column)), s0 = sexp
|
78
|
+
extract_idents(s0) + [Ident[:method, op_name.sub(/=\z/, ''), op_column]]
|
79
|
+
when :assoclist_from_args, :bare_assoc_hash, :dyna_symbol, :paren, :string_embexpr,
|
80
|
+
:regexp_literal, :xstring_literal
|
81
|
+
sexp[1].flat_map {|s| extract_idents(s) }
|
82
|
+
when :command
|
83
|
+
[sexp[2], sexp[1]].flat_map {|s| extract_idents(s) }
|
84
|
+
when :assoc_new, :dot2, :dot3, :string_content
|
85
|
+
sexp[1..-1].flat_map {|s| extract_idents(s) }
|
86
|
+
when :unary
|
87
|
+
handle_columnless_ident([], sexp[1], extract_idents(sexp[2]))
|
88
|
+
when :binary
|
89
|
+
op = sexp[2]
|
90
|
+
if AND_OR_OPS.include?(op)
|
91
|
+
extract_idents(sexp[1]) + [Branch[extract_idents(sexp[3]), []]]
|
92
|
+
else
|
93
|
+
handle_columnless_ident(extract_idents(sexp[1]), op, extract_idents(sexp[3]))
|
94
|
+
end
|
95
|
+
when :call
|
96
|
+
with_safe_op = sexp[2] == :"&."
|
97
|
+
if sexp[3] == :call
|
98
|
+
handle_columnless_ident(extract_idents(sexp[1]), :call, [], with_safe_op)
|
99
|
+
else
|
100
|
+
extract_idents(sexp[1]) + (with_safe_op ? [Branch[extract_idents(sexp[3]), []]] : extract_idents(sexp[3]))
|
101
|
+
end
|
102
|
+
when :array
|
103
|
+
sexp[1] ? sexp[1].flat_map {|s| extract_idents(s) } : []
|
104
|
+
when :command_call
|
105
|
+
[sexp[1], sexp[4], sexp[3]].flat_map {|s| extract_idents(s) }
|
106
|
+
when :aref
|
107
|
+
handle_columnless_ident(extract_idents(sexp[1]), :[], extract_idents(sexp[2]))
|
108
|
+
when :method_add_arg
|
109
|
+
idents = extract_idents(sexp[1])
|
110
|
+
if idents.empty?
|
111
|
+
# idents may be empty(e.g. ->{}.())
|
112
|
+
extract_idents(sexp[2])
|
113
|
+
else
|
114
|
+
if idents[-1].kind_of?(Branch) and idents[-1][1].empty?
|
115
|
+
# Safe navigation operator is used. See :call clause also.
|
116
|
+
idents[0..-2] + [Branch[extract_idents(sexp[2]) + idents[-1][0], []]]
|
117
|
+
else
|
118
|
+
idents[0..-2] + extract_idents(sexp[2]) + [idents[-1]]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
when :args_add_block
|
122
|
+
_, (tag, ss0, *ss1), _ = sexp
|
123
|
+
if tag == :args_add_star
|
124
|
+
(ss0 + ss1).flat_map {|s| extract_idents(s) }
|
125
|
+
else
|
126
|
+
sexp[1].flat_map {|s| extract_idents(s) }
|
127
|
+
end
|
128
|
+
when :vcall
|
129
|
+
_, (tag, name, (_, column)) = sexp
|
130
|
+
if tag == :@ident
|
131
|
+
[Ident[@proc_local_variables.include?(name) ? :ref : :method, name, column]]
|
132
|
+
else
|
133
|
+
[]
|
134
|
+
end
|
135
|
+
when :program
|
136
|
+
_, ((tag0, (tag1, (tag2, (tag3, mname, _)), _), (tag4, _, ss))) = sexp
|
137
|
+
if tag0 == :method_add_block and tag1 == :method_add_arg and tag2 == :fcall and
|
138
|
+
(tag3 == :@ident or tag3 == :@const) and mname == @assertion_method_name and (tag4 == :brace_block or tag4 == :do_block)
|
139
|
+
ss.flat_map {|s| extract_idents(s) }
|
140
|
+
else
|
141
|
+
_, (s, *) = sexp
|
142
|
+
extract_idents(s)
|
143
|
+
end
|
144
|
+
when :ifop
|
145
|
+
_, s0, s1, s2 = sexp
|
146
|
+
[*extract_idents(s0), Branch[extract_idents(s1), extract_idents(s2)]]
|
147
|
+
when :if_mod, :unless_mod
|
148
|
+
_, s0, s1 = sexp
|
149
|
+
[*extract_idents(s0), Branch[extract_idents(s1), []]]
|
150
|
+
when :var_ref, :var_field
|
151
|
+
_, (tag, ref_name, (_, column)) = sexp
|
152
|
+
case tag
|
153
|
+
when :@kw
|
154
|
+
if ref_name == 'self'
|
155
|
+
[Ident[:ref, 'self', column]]
|
156
|
+
else
|
157
|
+
[]
|
158
|
+
end
|
159
|
+
when :@ident, :@const, :@cvar, :@ivar, :@gvar
|
160
|
+
[Ident[:ref, ref_name, column]]
|
161
|
+
else
|
162
|
+
[]
|
163
|
+
end
|
164
|
+
when :@ident, :@const, :@op
|
165
|
+
_, method_name, (_, column) = sexp
|
166
|
+
[Ident[:method, method_name, column]]
|
167
|
+
else
|
168
|
+
[]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def str_indices(str, re, offset, limit)
|
173
|
+
idx = str.index(re, offset)
|
174
|
+
if idx and idx <= limit
|
175
|
+
[idx, *str_indices(str, re, idx + 1, limit)]
|
176
|
+
else
|
177
|
+
[]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
MID2SRCTXT = {
|
182
|
+
:[] => '[',
|
183
|
+
:+@ => '+',
|
184
|
+
:-@ => '-',
|
185
|
+
:call => '('
|
186
|
+
}
|
187
|
+
|
188
|
+
def handle_columnless_ident(left_idents, mid, right_idents, with_safe_op = false)
|
189
|
+
left_max = left_idents.flatten.max_by(&:column)
|
190
|
+
right_min = right_idents.flatten.min_by(&:column)
|
191
|
+
bg = left_max ? left_max.column + left_max.name.length : 0
|
192
|
+
ed = right_min ? right_min.column - 1 : @line_for_parsing.length - 1
|
193
|
+
mname = mid.to_s
|
194
|
+
srctxt = MID2SRCTXT[mid] || mname
|
195
|
+
re = /
|
196
|
+
#{'\b' if /\A\w/ =~ srctxt}
|
197
|
+
#{Regexp.escape(srctxt)}
|
198
|
+
#{'\b' if /\w\z/ =~ srctxt}
|
199
|
+
/x
|
200
|
+
indices = str_indices(@line_for_parsing, re, bg, ed)
|
201
|
+
if indices.length == 1 or !(right_idents.empty? and left_idents.empty?)
|
202
|
+
ident = Ident[:method, mname, right_idents.empty? ? indices.first : indices.last]
|
203
|
+
left_idents + right_idents + (with_safe_op ? [Branch[[ident], []]] : [ident])
|
204
|
+
else
|
205
|
+
left_idents + right_idents
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def collect_paths(idents, prefixes = [[]], index = 0)
|
210
|
+
if index < idents.length
|
211
|
+
node = idents[index]
|
212
|
+
if node.kind_of?(Branch)
|
213
|
+
prefixes = node.flat_map {|n| collect_paths(n, prefixes, 0) }
|
214
|
+
else
|
215
|
+
prefixes = prefixes.empty? ? [[node]] : prefixes.map {|prefix| prefix + [node] }
|
216
|
+
end
|
217
|
+
collect_paths(idents, prefixes, index + 1)
|
218
|
+
else
|
219
|
+
prefixes
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
class DummyParser < Parser
|
224
|
+
def initialize
|
225
|
+
super('', nil, nil, TOPLEVEL_BINDING)
|
226
|
+
end
|
227
|
+
|
228
|
+
def idents
|
229
|
+
[]
|
230
|
+
end
|
231
|
+
|
232
|
+
def call_paths
|
233
|
+
[]
|
234
|
+
end
|
235
|
+
end
|
236
|
+
DUMMY = DummyParser.new
|
237
|
+
end
|
238
|
+
private_constant :Parser
|
239
|
+
end
|