relevance-rcov 0.9.3-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/BLURB +111 -0
  2. data/LICENSE +53 -0
  3. data/Rakefile +94 -0
  4. data/THANKS +110 -0
  5. data/bin/rcov +514 -0
  6. data/doc/readme_for_api.markdown +22 -0
  7. data/doc/readme_for_emacs.markdown +52 -0
  8. data/doc/readme_for_rake.markdown +51 -0
  9. data/doc/readme_for_vim.markdown +34 -0
  10. data/editor-extensions/rcov.el +131 -0
  11. data/editor-extensions/rcov.vim +38 -0
  12. data/ext/java/src/CallsiteHook.java +137 -0
  13. data/ext/java/src/CoverageHook.java +117 -0
  14. data/ext/java/src/RcovHook.java +9 -0
  15. data/ext/java/src/RcovrtService.java +130 -0
  16. data/lib/rcov.rb +33 -0
  17. data/lib/rcov/call_site_analyzer.rb +225 -0
  18. data/lib/rcov/code_coverage_analyzer.rb +268 -0
  19. data/lib/rcov/coverage_info.rb +36 -0
  20. data/lib/rcov/differential_analyzer.rb +116 -0
  21. data/lib/rcov/file_statistics.rb +334 -0
  22. data/lib/rcov/formatters.rb +13 -0
  23. data/lib/rcov/formatters/base_formatter.rb +173 -0
  24. data/lib/rcov/formatters/failure_report.rb +15 -0
  25. data/lib/rcov/formatters/full_text_report.rb +48 -0
  26. data/lib/rcov/formatters/html_coverage.rb +274 -0
  27. data/lib/rcov/formatters/html_erb_template.rb +62 -0
  28. data/lib/rcov/formatters/text_coverage_diff.rb +193 -0
  29. data/lib/rcov/formatters/text_report.rb +32 -0
  30. data/lib/rcov/formatters/text_summary.rb +11 -0
  31. data/lib/rcov/lowlevel.rb +146 -0
  32. data/lib/rcov/rcovtask.rb +155 -0
  33. data/lib/rcov/templates/detail.html.erb +64 -0
  34. data/lib/rcov/templates/index.html.erb +93 -0
  35. data/lib/rcov/templates/jquery-1.3.2.min.js +19 -0
  36. data/lib/rcov/templates/jquery.tablesorter.min.js +15 -0
  37. data/lib/rcov/templates/print.css +12 -0
  38. data/lib/rcov/templates/rcov.js +42 -0
  39. data/lib/rcov/templates/screen.css +270 -0
  40. data/lib/rcov/version.rb +10 -0
  41. data/lib/rcovrt.jar +0 -0
  42. data/setup.rb +1588 -0
  43. data/test/assets/sample_01.rb +7 -0
  44. data/test/assets/sample_02.rb +5 -0
  45. data/test/assets/sample_03.rb +20 -0
  46. data/test/assets/sample_04.rb +10 -0
  47. data/test/assets/sample_05-new.rb +17 -0
  48. data/test/assets/sample_05-old.rb +13 -0
  49. data/test/assets/sample_05.rb +17 -0
  50. data/test/assets/sample_06.rb +8 -0
  51. data/test/call_site_analyzer_test.rb +171 -0
  52. data/test/code_coverage_analyzer_test.rb +219 -0
  53. data/test/file_statistics_test.rb +471 -0
  54. data/test/functional_test.rb +91 -0
  55. data/test/turn_off_rcovrt.rb +4 -0
  56. metadata +115 -0
@@ -0,0 +1,268 @@
1
+ module Rcov
2
+ # A CodeCoverageAnalyzer is responsible for tracing code execution and
3
+ # returning code coverage and execution count information.
4
+ #
5
+ # Note that you must <tt>require 'rcov'</tt> before the code you want to
6
+ # analyze is parsed (i.e. before it gets loaded or required). You can do that
7
+ # by either invoking ruby with the <tt>-rrcov</tt> command-line option or
8
+ # just:
9
+ # require 'rcov'
10
+ # require 'mycode'
11
+ # # ....
12
+ #
13
+ # == Example
14
+ #
15
+ # analyzer = Rcov::CodeCoverageAnalyzer.new
16
+ # analyzer.run_hooked do
17
+ # do_foo
18
+ # # all the code executed as a result of this method call is traced
19
+ # end
20
+ # # ....
21
+ #
22
+ # analyzer.run_hooked do
23
+ # do_bar
24
+ # # the code coverage information generated in this run is aggregated
25
+ # # to the previously recorded one
26
+ # end
27
+ #
28
+ # analyzer.analyzed_files # => ["foo.rb", "bar.rb", ... ]
29
+ # lines, marked_info, count_info = analyzer.data("foo.rb")
30
+ #
31
+ # In this example, two pieces of code are monitored, and the data generated in
32
+ # both runs are aggregated. +lines+ is an array of strings representing the
33
+ # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
34
+ # true values indicating whether the corresponding lines of code were reported
35
+ # as executed by Ruby. +count_info+ is an array of integers representing how
36
+ # many times each line of code has been executed (more precisely, how many
37
+ # events where reported by Ruby --- a single line might correspond to several
38
+ # events, e.g. many method calls).
39
+ #
40
+ # You can have several CodeCoverageAnalyzer objects at a time, and it is
41
+ # possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
42
+ # analyzer will manage its data separately. Note however that no special
43
+ # provision is taken to ignore code executed "inside" the CodeCoverageAnalyzer
44
+ # class. At any rate this will not pose a problem since it's easy to ignore it
45
+ # manually: just don't do
46
+ # lines, coverage, counts = analyzer.data("/path/to/lib/rcov.rb")
47
+ # if you're not interested in that information.
48
+ class CodeCoverageAnalyzer < DifferentialAnalyzer
49
+ @hook_level = 0
50
+ # defined this way instead of attr_accessor so that it's covered
51
+ def self.hook_level # :nodoc:
52
+ @hook_level
53
+ end
54
+
55
+ def self.hook_level=(x) # :nodoc:
56
+ @hook_level = x
57
+ end
58
+
59
+ def initialize
60
+ @script_lines__ = SCRIPT_LINES__
61
+ super(:install_coverage_hook, :remove_coverage_hook,
62
+ :reset_coverage)
63
+ end
64
+
65
+ # Return an array with the names of the files whose code was executed inside
66
+ # the block given to #run_hooked or between #install_hook and #remove_hook.
67
+ def analyzed_files
68
+ update_script_lines__
69
+ raw_data_relative.select do |file, lines|
70
+ @script_lines__.has_key?(file)
71
+ end.map{|fname,| fname}
72
+ end
73
+
74
+ # Return the available data about the requested file, or nil if none of its
75
+ # code was executed or it cannot be found.
76
+ # The return value is an array with three elements:
77
+ # lines, marked_info, count_info = analyzer.data("foo.rb")
78
+ # +lines+ is an array of strings representing the
79
+ # source code of <tt>foo.rb</tt>. +marked_info+ is an array holding false,
80
+ # true values indicating whether the corresponding lines of code were reported
81
+ # as executed by Ruby. +count_info+ is an array of integers representing how
82
+ # many times each line of code has been executed (more precisely, how many
83
+ # events where reported by Ruby --- a single line might correspond to several
84
+ # events, e.g. many method calls).
85
+ #
86
+ # The returned data corresponds to the aggregation of all the statistics
87
+ # collected in each #run_hooked or #install_hook/#remove_hook runs. You can
88
+ # reset the data at any time with #reset to start from scratch.
89
+ def data(filename)
90
+ raw_data = raw_data_relative
91
+ update_script_lines__
92
+ unless @script_lines__.has_key?(filename) &&
93
+ raw_data.has_key?(filename)
94
+ return nil
95
+ end
96
+ refine_coverage_info(@script_lines__[filename], raw_data[filename])
97
+ end
98
+
99
+ # Data for the first file matching the given regexp.
100
+ # See #data.
101
+ def data_matching(filename_re)
102
+ raw_data = raw_data_relative
103
+ update_script_lines__
104
+
105
+ match = raw_data.keys.sort.grep(filename_re).first
106
+ return nil unless match
107
+
108
+ refine_coverage_info(@script_lines__[match], raw_data[match])
109
+ end
110
+
111
+ # Execute the code in the given block, monitoring it in order to gather
112
+ # information about which code was executed.
113
+ def run_hooked; super end
114
+
115
+ # Start monitoring execution to gather code coverage and execution count
116
+ # information. Such data will be collected until #remove_hook is called.
117
+ #
118
+ # Use #run_hooked instead if possible.
119
+ def install_hook; super end
120
+
121
+ # Stop collecting code coverage and execution count information.
122
+ # #remove_hook will also stop collecting info if it is run inside a
123
+ # #run_hooked block.
124
+ def remove_hook; super end
125
+
126
+ # Remove the data collected so far. The coverage and execution count
127
+ # "history" will be erased, and further collection will start from scratch:
128
+ # no code is considered executed, and therefore all execution counts are 0.
129
+ # Right after #reset, #analyzed_files will return an empty array, and
130
+ # #data(filename) will return nil.
131
+ def reset; super end
132
+
133
+ def dump_coverage_info(formatters) # :nodoc:
134
+ update_script_lines__
135
+ raw_data_relative.each do |file, lines|
136
+ next if @script_lines__.has_key?(file) == false
137
+ lines = @script_lines__[file]
138
+ raw_coverage_array = raw_data_relative[file]
139
+
140
+ line_info, marked_info,
141
+ count_info = refine_coverage_info(lines, raw_coverage_array)
142
+ formatters.each do |formatter|
143
+ formatter.add_file(file, line_info, marked_info, count_info)
144
+ end
145
+ end
146
+ formatters.each{|formatter| formatter.execute}
147
+ end
148
+
149
+ private
150
+
151
+ def data_default; {} end
152
+
153
+ def raw_data_absolute
154
+ Rcov::RCOV__.generate_coverage_info
155
+ end
156
+
157
+ def aggregate_data(aggregated_data, delta)
158
+ delta.each_pair do |file, cov_arr|
159
+ dest = (aggregated_data[file] ||= Array.new(cov_arr.size, 0))
160
+ cov_arr.each_with_index{|x,i| dest[i] += x.to_i}
161
+ end
162
+ end
163
+
164
+ def compute_raw_data_difference(first, last)
165
+ difference = {}
166
+ last.each_pair do |fname, cov_arr|
167
+ unless first.has_key?(fname)
168
+ difference[fname] = cov_arr.clone
169
+ else
170
+ orig_arr = first[fname]
171
+ diff_arr = Array.new(cov_arr.size, 0)
172
+ changed = false
173
+ cov_arr.each_with_index do |x, i|
174
+ diff_arr[i] = diff = (x || 0) - (orig_arr[i] || 0)
175
+ changed = true if diff != 0
176
+ end
177
+ difference[fname] = diff_arr if changed
178
+ end
179
+ end
180
+ difference
181
+ end
182
+
183
+ def refine_coverage_info(lines, covers)
184
+ marked_info = []
185
+ count_info = []
186
+ lines.size.times do |i|
187
+ c = covers[i]
188
+ marked_info << ((c && c > 0) ? true : false)
189
+ count_info << (c || 0)
190
+ end
191
+
192
+ script_lines_workaround(lines, marked_info, count_info)
193
+ end
194
+
195
+ # Try to detect repeated data, based on observed repetitions in line_info:
196
+ # this is a workaround for SCRIPT_LINES__[filename] including as many copies
197
+ # of the file as the number of times it was parsed.
198
+ def script_lines_workaround(line_info, coverage_info, count_info)
199
+ is_repeated = lambda do |div|
200
+ n = line_info.size / div
201
+ break false unless line_info.size % div == 0 && n > 1
202
+ different = false
203
+ n.times do |i|
204
+
205
+ things = (0...div).map { |j| line_info[i + j * n] }
206
+ if things.uniq.size != 1
207
+ different = true
208
+ break
209
+ end
210
+ end
211
+
212
+ ! different
213
+ end
214
+
215
+ factors = braindead_factorize(line_info.size)
216
+ factors.each do |n|
217
+ if is_repeated[n]
218
+ line_info = line_info[0, line_info.size / n]
219
+ coverage_info = coverage_info[0, coverage_info.size / n]
220
+ count_info = count_info[0, count_info.size / n]
221
+ end
222
+ end if factors.size > 1 # don't even try if it's prime
223
+
224
+ [line_info, coverage_info, count_info]
225
+ end
226
+
227
+ def braindead_factorize(num)
228
+ return [0] if num == 0
229
+ return [-1] + braindead_factorize(-num) if num < 0
230
+ factors = []
231
+ while num % 2 == 0
232
+ factors << 2
233
+ num /= 2
234
+ end
235
+ size = num
236
+ n = 3
237
+ max = Math.sqrt(num)
238
+ while n <= max && n <= size
239
+ while size % n == 0
240
+ size /= n
241
+ factors << n
242
+ end
243
+ n += 2
244
+ end
245
+ factors << size if size != 1
246
+ factors
247
+ end
248
+
249
+ def update_script_lines__
250
+ @script_lines__ = @script_lines__.merge(SCRIPT_LINES__)
251
+ end
252
+
253
+ public
254
+
255
+ def marshal_dump # :nodoc:
256
+ # @script_lines__ is updated just before serialization so as to avoid
257
+ # missing files in SCRIPT_LINES__
258
+ ivs = {}
259
+ update_script_lines__
260
+ instance_variables.each{|iv| ivs[iv] = instance_variable_get(iv)}
261
+ ivs
262
+ end
263
+
264
+ def marshal_load(ivs) # :nodoc:
265
+ ivs.each_pair{|iv, val| instance_variable_set(iv, val)}
266
+ end
267
+ end # CodeCoverageAnalyzer
268
+ end
@@ -0,0 +1,36 @@
1
+ # Rcov::CoverageInfo is but a wrapper for an array, with some additional
2
+ # checks. It is returned by FileStatistics#coverage.
3
+ class CoverageInfo
4
+ def initialize(coverage_array)
5
+ @cover = coverage_array.clone
6
+ end
7
+
8
+ # Return the coverage status for the requested line. There are four possible
9
+ # return values:
10
+ # * nil if there's no information for the requested line (i.e. it doesn't exist)
11
+ # * true if the line was reported by Ruby as executed
12
+ # * :inferred if rcov inferred it was executed, despite not being reported
13
+ # by Ruby.
14
+ # * false otherwise, i.e. if it was not reported by Ruby and rcov's
15
+ # heuristics indicated that it was not executed
16
+ def [](line)
17
+ @cover[line]
18
+ end
19
+
20
+ def []=(line, val) # :nodoc:
21
+ unless [true, false, :inferred].include? val
22
+ raise RuntimeError, "What does #{val} mean?"
23
+ end
24
+ return if line < 0 || line >= @cover.size
25
+ @cover[line] = val
26
+ end
27
+
28
+ # Return an Array holding the code coverage information.
29
+ def to_a
30
+ @cover.clone
31
+ end
32
+
33
+ def method_missing(meth, *a, &b) # :nodoc:
34
+ @cover.send(meth, *a, &b)
35
+ end
36
+ end
@@ -0,0 +1,116 @@
1
+ module Rcov
2
+ class DifferentialAnalyzer
3
+ require 'thread'
4
+ @@mutex = Mutex.new
5
+
6
+ def initialize(install_hook_meth, remove_hook_meth, reset_meth)
7
+ @cache_state = :wait
8
+ @start_raw_data = data_default
9
+ @end_raw_data = data_default
10
+ @aggregated_data = data_default
11
+ @install_hook_meth = install_hook_meth
12
+ @remove_hook_meth= remove_hook_meth
13
+ @reset_meth= reset_meth
14
+ end
15
+
16
+ # Execute the code in the given block, monitoring it in order to gather
17
+ # information about which code was executed.
18
+ def run_hooked
19
+ install_hook
20
+ yield
21
+ ensure
22
+ remove_hook
23
+ end
24
+
25
+ # Start monitoring execution to gather information. Such data will be
26
+ # collected until #remove_hook is called.
27
+ #
28
+ # Use #run_hooked instead if possible.
29
+ def install_hook
30
+ @start_raw_data = raw_data_absolute
31
+ Rcov::RCOV__.send(@install_hook_meth)
32
+ @cache_state = :hooked
33
+ @@mutex.synchronize{ self.class.hook_level += 1 }
34
+ end
35
+
36
+ # Stop collecting information.
37
+ # #remove_hook will also stop collecting info if it is run inside a
38
+ # #run_hooked block.
39
+ def remove_hook
40
+ @@mutex.synchronize do
41
+ self.class.hook_level -= 1
42
+ Rcov::RCOV__.send(@remove_hook_meth) if self.class.hook_level == 0
43
+ end
44
+ @end_raw_data = raw_data_absolute
45
+ @cache_state = :done
46
+ # force computation of the stats for the traced code in this run;
47
+ # we cannot simply let it be if self.class.hook_level == 0 because
48
+ # some other analyzer could install a hook, causing the raw_data_absolute
49
+ # to change again.
50
+ # TODO: lazy computation of raw_data_relative, only when the hook gets
51
+ # activated again.
52
+ raw_data_relative
53
+ end
54
+
55
+ # Remove the data collected so far. Further collection will start from
56
+ # scratch.
57
+ def reset
58
+ @@mutex.synchronize do
59
+ if self.class.hook_level == 0
60
+ # Unfortunately there's no way to report this as covered with rcov:
61
+ # if we run the tests under rcov self.class.hook_level will be >= 1 !
62
+ # It is however executed when we run the tests normally.
63
+ Rcov::RCOV__.send(@reset_meth)
64
+ @start_raw_data = data_default
65
+ @end_raw_data = data_default
66
+ else
67
+ @start_raw_data = @end_raw_data = raw_data_absolute
68
+ end
69
+ @raw_data_relative = data_default
70
+ @aggregated_data = data_default
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def data_default
77
+ raise "must be implemented by the subclass"
78
+ end
79
+
80
+ def self.hook_level
81
+ raise "must be implemented by the subclass"
82
+ end
83
+
84
+ def raw_data_absolute
85
+ raise "must be implemented by the subclass"
86
+ end
87
+
88
+ def aggregate_data(aggregated_data, delta)
89
+ raise "must be implemented by the subclass"
90
+ end
91
+
92
+ def compute_raw_data_difference(first, last)
93
+ raise "must be implemented by the subclass"
94
+ end
95
+
96
+ private
97
+
98
+ def raw_data_relative
99
+ case @cache_state
100
+ when :wait
101
+ return @aggregated_data
102
+ when :hooked
103
+ new_start = raw_data_absolute
104
+ new_diff = compute_raw_data_difference(@start_raw_data, new_start)
105
+ @start_raw_data = new_start
106
+ when :done
107
+ @cache_state = :wait
108
+ new_diff = compute_raw_data_difference(@start_raw_data,
109
+ @end_raw_data)
110
+ end
111
+
112
+ aggregate_data(@aggregated_data, new_diff)
113
+ @aggregated_data
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,334 @@
1
+ module Rcov
2
+ # A FileStatistics object associates a filename to:
3
+ # 1. its source code
4
+ # 2. the per-line coverage information after correction using rcov's heuristics
5
+ # 3. the per-line execution counts
6
+ #
7
+ # A FileStatistics object can be therefore be built given the filename, the
8
+ # associated source code, and an array holding execution counts (i.e. how many
9
+ # times each line has been executed).
10
+ #
11
+ # FileStatistics is relatively intelligent: it handles normal comments,
12
+ # <tt>=begin/=end</tt>, heredocs, many multiline-expressions... It uses a
13
+ # number of heuristics to determine what is code and what is a comment, and to
14
+ # refine the initial (incomplete) coverage information.
15
+ #
16
+ # Basic usage is as follows:
17
+ # sf = FileStatistics.new("foo.rb", ["puts 1", "if true &&", " false",
18
+ # "puts 2", "end"], [1, 1, 0, 0, 0])
19
+ # sf.num_lines # => 5
20
+ # sf.num_code_lines # => 5
21
+ # sf.coverage[2] # => true
22
+ # sf.coverage[3] # => :inferred
23
+ # sf.code_coverage # => 0.6
24
+ #
25
+ # The array of strings representing the source code and the array of execution
26
+ # counts would normally be obtained from a Rcov::CodeCoverageAnalyzer.
27
+ class FileStatistics
28
+ attr_reader :name, :lines, :coverage, :counts
29
+ def initialize(name, lines, counts, comments_run_by_default = false)
30
+ @name = name
31
+ @lines = lines
32
+ initial_coverage = counts.map{|x| (x || 0) > 0 ? true : false }
33
+ @coverage = CoverageInfo.new initial_coverage
34
+ @counts = counts
35
+ @is_begin_comment = nil
36
+ # points to the line defining the heredoc identifier
37
+ # but only if it was marked (we don't care otherwise)
38
+ @heredoc_start = Array.new(lines.size, false)
39
+ @multiline_string_start = Array.new(lines.size, false)
40
+ extend_heredocs
41
+ find_multiline_strings
42
+ precompute_coverage comments_run_by_default
43
+ end
44
+
45
+ # Merge code coverage and execution count information.
46
+ # As for code coverage, a line will be considered
47
+ # * covered for sure (true) if it is covered in either +self+ or in the
48
+ # +coverage+ array
49
+ # * considered <tt>:inferred</tt> if the neither +self+ nor the +coverage+ array
50
+ # indicate that it was definitely executed, but it was <tt>inferred</tt>
51
+ # in either one
52
+ # * not covered (<tt>false</tt>) if it was uncovered in both
53
+ #
54
+ # Execution counts are just summated on a per-line basis.
55
+ def merge(lines, coverage, counts)
56
+ coverage.each_with_index do |v, idx|
57
+ case @coverage[idx]
58
+ when :inferred
59
+ @coverage[idx] = v || @coverage[idx]
60
+ when false
61
+ @coverage[idx] ||= v
62
+ end
63
+ end
64
+ counts.each_with_index{|v, idx| @counts[idx] += v }
65
+ precompute_coverage false
66
+ end
67
+
68
+ # Total coverage rate if comments are also considered "executable", given as
69
+ # a fraction, i.e. from 0 to 1.0.
70
+ # A comment is attached to the code following it (RDoc-style): it will be
71
+ # considered executed if the the next statement was executed.
72
+ def total_coverage
73
+ return 0 if @coverage.size == 0
74
+ @coverage.inject(0.0) {|s,a| s + (a ? 1:0) } / @coverage.size
75
+ end
76
+
77
+ # Code coverage rate: fraction of lines of code executed, relative to the
78
+ # total amount of lines of code (loc). Returns a float from 0 to 1.0.
79
+ def code_coverage
80
+ indices = (0...@lines.size).select{|i| is_code? i }
81
+ return 0 if indices.size == 0
82
+ count = 0
83
+ indices.each {|i| count += 1 if @coverage[i] }
84
+ 1.0 * count / indices.size
85
+ end
86
+
87
+ def code_coverage_for_report
88
+ code_coverage * 100
89
+ end
90
+
91
+ def total_coverage_for_report
92
+ total_coverage * 100
93
+ end
94
+
95
+ # Number of lines of code (loc).
96
+ def num_code_lines
97
+ (0...@lines.size).select{|i| is_code? i}.size
98
+ end
99
+
100
+ # Total number of lines.
101
+ def num_lines
102
+ @lines.size
103
+ end
104
+
105
+ # Returns true if the given line number corresponds to code, as opposed to a
106
+ # comment (either # or =begin/=end blocks).
107
+ def is_code?(lineno)
108
+ unless @is_begin_comment
109
+ @is_begin_comment = Array.new(@lines.size, false)
110
+ pending = []
111
+ state = :code
112
+ @lines.each_with_index do |line, index|
113
+ case state
114
+ when :code
115
+ if /^=begin\b/ =~ line
116
+ state = :comment
117
+ pending << index
118
+ end
119
+ when :comment
120
+ pending << index
121
+ if /^=end\b/ =~ line
122
+ state = :code
123
+ pending.each{|idx| @is_begin_comment[idx] = true}
124
+ pending.clear
125
+ end
126
+ end
127
+ end
128
+ end
129
+ @lines[lineno] && !@is_begin_comment[lineno] &&
130
+ @lines[lineno] !~ /^\s*(#|$)/
131
+ end
132
+
133
+ private
134
+
135
+ def find_multiline_strings
136
+ state = :awaiting_string
137
+ wanted_delimiter = nil
138
+ string_begin_line = 0
139
+ @lines.each_with_index do |line, i|
140
+ matching_delimiters = Hash.new{|h,k| k}
141
+ matching_delimiters.update("{" => "}", "[" => "]", "(" => ")")
142
+ case state
143
+ when :awaiting_string
144
+ # very conservative, doesn't consider the last delimited string but
145
+ # only the very first one
146
+ if md = /^[^#]*%(?:[qQ])?(.)/.match(line)
147
+ if !/"%"/.match(line)
148
+ wanted_delimiter = /(?!\\).#{Regexp.escape(matching_delimiters[md[1]])}/
149
+ # check if closed on the very same line
150
+ # conservative again, we might have several quoted strings with the
151
+ # same delimiter on the same line, leaving the last one open
152
+ unless wanted_delimiter.match(md.post_match)
153
+ state = :want_end_delimiter
154
+ string_begin_line = i
155
+ end
156
+ end
157
+ end
158
+ when :want_end_delimiter
159
+ @multiline_string_start[i] = string_begin_line
160
+ if wanted_delimiter.match(line)
161
+ state = :awaiting_string
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ def precompute_coverage(comments_run_by_default = true)
168
+ changed = false
169
+ lastidx = lines.size - 1
170
+ if (!is_code?(lastidx) || /^__END__$/ =~ @lines[-1]) && !@coverage[lastidx]
171
+ # mark the last block of comments
172
+ @coverage[lastidx] ||= :inferred
173
+ (lastidx-1).downto(0) do |i|
174
+ break if is_code?(i)
175
+ @coverage[i] ||= :inferred
176
+ end
177
+ end
178
+ (0...lines.size).each do |i|
179
+ next if @coverage[i]
180
+ line = @lines[i]
181
+ if /^\s*(begin|ensure|else|case)\s*(?:#.*)?$/ =~ line && next_expr_marked?(i) or
182
+ /^\s*(?:end|\})\s*(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
183
+ /^\s*(?:end\b|\})/ =~ line && prev_expr_marked?(i) && next_expr_marked?(i) or
184
+ /^\s*rescue\b/ =~ line && next_expr_marked?(i) or
185
+ /(do|\{)\s*(\|[^|]*\|\s*)?(?:#.*)?$/ =~ line && next_expr_marked?(i) or
186
+ prev_expr_continued?(i) && prev_expr_marked?(i) or
187
+ comments_run_by_default && !is_code?(i) or
188
+ /^\s*((\)|\]|\})\s*)+(?:#.*)?$/ =~ line && prev_expr_marked?(i) or
189
+ prev_expr_continued?(i+1) && next_expr_marked?(i)
190
+ @coverage[i] ||= :inferred
191
+ changed = true
192
+ end
193
+ end
194
+ (@lines.size-1).downto(0) do |i|
195
+ next if @coverage[i]
196
+ if !is_code?(i) and @coverage[i+1]
197
+ @coverage[i] = :inferred
198
+ changed = true
199
+ end
200
+ end
201
+
202
+ extend_heredocs if changed
203
+
204
+ # if there was any change, we have to recompute; we'll eventually
205
+ # reach a fixed point and stop there
206
+ precompute_coverage(comments_run_by_default) if changed
207
+ end
208
+
209
+ require 'strscan'
210
+ def extend_heredocs
211
+ i = 0
212
+ while i < @lines.size
213
+ unless is_code? i
214
+ i += 1
215
+ next
216
+ end
217
+ #FIXME: using a restrictive regexp so that only <<[A-Z_a-z]\w*
218
+ # matches when unquoted, so as to avoid problems with 1<<2
219
+ # (keep in mind that whereas puts <<2 is valid, puts 1<<2 is a
220
+ # parse error, but a = 1<<2 is of course fine)
221
+ scanner = StringScanner.new(@lines[i])
222
+ j = k = i
223
+ loop do
224
+ scanned_text = scanner.search_full(/<<(-?)(?:(['"`])((?:(?!\2).)+)\2|([A-Z_a-z]\w*))/, true, true)
225
+ # k is the first line after the end delimiter for the last heredoc
226
+ # scanned so far
227
+ unless scanner.matched?
228
+ i = k
229
+ break
230
+ end
231
+ term = scanner[3] || scanner[4]
232
+ # try to ignore symbolic bitshifts like 1<<LSHIFT
233
+ ident_text = "<<#{scanner[1]}#{scanner[2]}#{term}#{scanner[2]}"
234
+ if scanned_text[/\d+\s*#{Regexp.escape(ident_text)}/]
235
+ # it was preceded by a number, ignore
236
+ i = k
237
+ break
238
+ end
239
+ must_mark = []
240
+ end_of_heredoc = (scanner[1] == "-") ? /^\s*#{Regexp.escape(term)}$/ : /^#{Regexp.escape(term)}$/
241
+ loop do
242
+ break if j == @lines.size
243
+ must_mark << j
244
+ if end_of_heredoc =~ @lines[j]
245
+ must_mark.each do |n|
246
+ @heredoc_start[n] = i
247
+ end
248
+ if (must_mark + [i]).any?{|lineidx| @coverage[lineidx]}
249
+ @coverage[i] ||= :inferred
250
+ must_mark.each{|lineidx| @coverage[lineidx] ||= :inferred}
251
+ end
252
+ # move the "first line after heredocs" index
253
+ if @lines[j+=1] =~ /^\s*\n$/
254
+ k = j
255
+ end
256
+ break
257
+ end
258
+ j += 1
259
+ end
260
+ end
261
+
262
+ i += 1
263
+ end
264
+ end
265
+
266
+ def next_expr_marked?(lineno)
267
+ return false if lineno >= @lines.size
268
+ found = false
269
+ idx = (lineno+1).upto(@lines.size-1) do |i|
270
+ next unless is_code? i
271
+ found = true
272
+ break i
273
+ end
274
+ return false unless found
275
+ @coverage[idx]
276
+ end
277
+
278
+ def prev_expr_marked?(lineno)
279
+ return false if lineno <= 0
280
+ found = false
281
+ idx = (lineno-1).downto(0) do |i|
282
+ next unless is_code? i
283
+ found = true
284
+ break i
285
+ end
286
+ return false unless found
287
+ @coverage[idx]
288
+ end
289
+
290
+ def prev_expr_continued?(lineno)
291
+ return false if lineno <= 0
292
+ return false if lineno >= @lines.size
293
+ found = false
294
+ if @multiline_string_start[lineno] &&
295
+ @multiline_string_start[lineno] < lineno
296
+ return true
297
+ end
298
+ # find index of previous code line
299
+ idx = (lineno-1).downto(0) do |i|
300
+ if @heredoc_start[i]
301
+ found = true
302
+ break @heredoc_start[i]
303
+ end
304
+ next unless is_code? i
305
+ found = true
306
+ break i
307
+ end
308
+ return false unless found
309
+ #TODO: write a comprehensive list
310
+ if is_code?(lineno) && /^\s*((\)|\]|\})\s*)+(?:#.*)?$/.match(@lines[lineno])
311
+ return true
312
+ end
313
+ #FIXME: / matches regexps too
314
+ # the following regexp tries to reject #{interpolation}
315
+ r = /(,|\.|\+|-|\*|\/|<|>|%|&&|\|\||<<|\(|\[|\{|=|and|or|\\)\s*(?:#(?![{$@]).*)?$/.match @lines[idx]
316
+ # try to see if a multi-line expression with opening, closing delimiters
317
+ # started on that line
318
+ [%w!( )!].each do |opening_str, closing_str|
319
+ # conservative: only consider nesting levels opened in that line, not
320
+ # previous ones too.
321
+ # next regexp considers interpolation too
322
+ line = @lines[idx].gsub(/#(?![{$@]).*$/, "")
323
+ opened = line.scan(/#{Regexp.escape(opening_str)}/).size
324
+ closed = line.scan(/#{Regexp.escape(closing_str)}/).size
325
+ return true if opened - closed > 0
326
+ end
327
+ if /(do|\{)\s*\|[^|]*\|\s*(?:#.*)?$/.match @lines[idx]
328
+ return false
329
+ end
330
+
331
+ r
332
+ end
333
+ end
334
+ end