bugwatch 0.1

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.
@@ -0,0 +1,803 @@
1
+ # Copyright (c) 2013, Groupon, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions
6
+ # are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # Neither the name of GROUPON nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require 'rubygems'
32
+ require 'sexp_processor'
33
+ require 'ruby_parser'
34
+ require 'optparse'
35
+ require 'timeout'
36
+
37
+ class File
38
+ RUBY19 = "<3".respond_to? :encoding unless defined? RUBY19
39
+
40
+ class << self
41
+ alias :binread :read unless RUBY19
42
+ end
43
+ end
44
+
45
+ class Flog < SexpProcessor
46
+ VERSION = '3.0.0'
47
+
48
+ THRESHOLD = 0.60
49
+ SCORES = Hash.new 1
50
+ BRANCHING = [ :and, :case, :else, :if, :or, :rescue, :until, :when, :while ]
51
+
52
+ ##
53
+ # Various non-call constructs
54
+
55
+ OTHER_SCORES = {
56
+ :alias => 2,
57
+ :assignment => 1,
58
+ :block => 1,
59
+ :block_pass => 1,
60
+ :branch => 1,
61
+ :lit_fixnum => 0.25,
62
+ :sclass => 5,
63
+ :super => 1,
64
+ :to_proc_icky! => 10,
65
+ :to_proc_lasgn => 15,
66
+ :to_proc_normal => 5,
67
+ :yield => 1,
68
+ }
69
+
70
+ ##
71
+ # Eval forms
72
+
73
+ SCORES.merge!(:define_method => 5,
74
+ :eval => 5,
75
+ :module_eval => 5,
76
+ :class_eval => 5,
77
+ :instance_eval => 5)
78
+
79
+ ##
80
+ # Various "magic" usually used for "clever code"
81
+
82
+ SCORES.merge!(:alias_method => 2,
83
+ :extend => 2,
84
+ :include => 2,
85
+ :instance_method => 2,
86
+ :instance_methods => 2,
87
+ :method_added => 2,
88
+ :method_defined? => 2,
89
+ :method_removed => 2,
90
+ :method_undefined => 2,
91
+ :private_class_method => 2,
92
+ :private_instance_methods => 2,
93
+ :private_method_defined? => 2,
94
+ :protected_instance_methods => 2,
95
+ :protected_method_defined? => 2,
96
+ :public_class_method => 2,
97
+ :public_instance_methods => 2,
98
+ :public_method_defined? => 2,
99
+ :remove_method => 2,
100
+ :send => 3,
101
+ :undef_method => 2)
102
+
103
+ ##
104
+ # Calls that are ALMOST ALWAYS ABUSED!
105
+
106
+ SCORES.merge!(:inject => 2)
107
+
108
+ @@no_class = :main
109
+ @@no_method = :none
110
+
111
+ attr_accessor :multiplier
112
+ attr_reader :calls, :option, :class_stack, :method_stack, :mass
113
+ attr_reader :method_locations
114
+
115
+ def self.plugins
116
+ @plugins ||= {}
117
+ end
118
+
119
+ # TODO: I think I want to do this more like hoe's plugin system. Generalize?
120
+ def self.load_plugins
121
+ loaded, found = {}, {}
122
+
123
+ Gem.find_files("flog/*.rb").reverse.each do |path|
124
+ found[File.basename(path, ".rb").intern] = path
125
+ end
126
+
127
+ found.each do |name, plugin|
128
+ next if loaded[name]
129
+ begin
130
+ warn "loading #{plugin}" # if $DEBUG
131
+ loaded[name] = load plugin
132
+ rescue LoadError => e
133
+ warn "error loading #{plugin.inspect}: #{e.message}. skipping..."
134
+ end
135
+ end
136
+
137
+ self.plugins.merge loaded
138
+
139
+ names = Flog.constants.map {|s| s.to_s}.reject {|n| n =~ /^[A-Z_]+$/}
140
+
141
+ names.each do |name|
142
+ # next unless Hoe.plugins.include? name.downcase.intern
143
+ mod = Flog.const_get(name)
144
+ next if Class === mod
145
+ warn "extend #{mod}" if $DEBUG
146
+ # self.extend mod
147
+ end
148
+ end
149
+
150
+ # REFACTOR: from flay
151
+ def self.expand_dirs_to_files *dirs
152
+ extensions = %w[rb rake]
153
+
154
+ dirs.flatten.map { |p|
155
+ if File.directory? p then
156
+ Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
157
+ else
158
+ p
159
+ end
160
+ }.flatten.sort
161
+ end
162
+
163
+ def self.parse_options args = ARGV
164
+ option = {
165
+ :quiet => false,
166
+ :continue => false,
167
+ :parser => RubyParser,
168
+ }
169
+
170
+ OptionParser.new do |opts|
171
+ opts.separator "Standard options:"
172
+
173
+ opts.on("-a", "--all", "Display all flog results, not top 60%.") do
174
+ option[:all] = true
175
+ end
176
+
177
+ opts.on("-b", "--blame", "Include blame information for methods.") do
178
+ option[:blame] = true
179
+ end
180
+
181
+ opts.on("-c", "--continue", "Continue despite syntax errors.") do
182
+ option[:continue] = true
183
+ end
184
+
185
+ opts.on("-d", "--details", "Show method details.") do
186
+ option[:details] = true
187
+ end
188
+
189
+ opts.on("-g", "--group", "Group and sort by class.") do
190
+ option[:group] = true
191
+ end
192
+
193
+ opts.on("-h", "--help", "Show this message.") do
194
+ puts opts
195
+ exit
196
+ end
197
+
198
+ opts.on("-I dir1,dir2,dir3", Array, "Add to LOAD_PATH.") do |dirs|
199
+ dirs.each do |dir|
200
+ $: << dir
201
+ end
202
+ end
203
+
204
+ opts.on("-m", "--methods-only", "Skip code outside of methods.") do
205
+ option[:methods] = true
206
+ end
207
+
208
+ opts.on("-q", "--quiet", "Don't show parse errors.") do
209
+ option[:quiet] = true
210
+ end
211
+
212
+ opts.on("-s", "--score", "Display total score only.") do
213
+ option[:score] = true
214
+ end
215
+
216
+ opts.on("-v", "--verbose", "Display progress during processing.") do
217
+ option[:verbose] = true
218
+ end
219
+
220
+ opts.on("--18", "Use a ruby 1.8 parser.") do
221
+ option[:parser] = Ruby18Parser
222
+ end
223
+
224
+ opts.on("--19", "Use a ruby 1.9 parser.") do
225
+ option[:parser] = Ruby19Parser
226
+ end
227
+
228
+ next if self.plugins.empty?
229
+ opts.separator "Plugin options:"
230
+
231
+ extra = self.methods.grep(/parse_options/) - %w(parse_options)
232
+
233
+ extra.sort.each do |msg|
234
+ self.send msg, opts, option
235
+ end
236
+
237
+ end.parse! Array(args)
238
+
239
+ option
240
+ end
241
+
242
+ ##
243
+ # Add a score to the tally. Score can be predetermined or looked up
244
+ # automatically. Uses multiplier for additional spankings.
245
+ # Spankings!
246
+
247
+ def add_to_score name, score = OTHER_SCORES[name]
248
+ @calls[signature][name] += score * @multiplier
249
+ end
250
+
251
+ ##
252
+ # really?
253
+
254
+ def average
255
+ return 0 if calls.size == 0
256
+ total / calls.size
257
+ end
258
+
259
+ ##
260
+ # Iterate over the calls sorted (descending) by score.
261
+
262
+ def each_by_score max = nil
263
+ my_totals = totals
264
+ current = 0
265
+
266
+ calls.sort_by { |k,v| -my_totals[k] }.each do |class_method, call_list|
267
+ score = my_totals[class_method]
268
+
269
+ yield class_method, score, call_list
270
+
271
+ current += score
272
+ break if max and current >= max
273
+ end
274
+ end
275
+
276
+ ##
277
+ # Flog the given files or directories. Smart. Deals with "-", syntax
278
+ # errors, and traversing subdirectories intelligently.
279
+
280
+ def flog(*files_or_dirs)
281
+ files = Flog.expand_dirs_to_files(*files_or_dirs)
282
+
283
+ files.each do |file|
284
+ # TODO: replace File.open to deal with "-"
285
+ ruby = file == '-' ? $stdin.read : File.binread(file)
286
+ flog_code(ruby, file)
287
+ end
288
+ end
289
+
290
+ def flog_code(ruby, file="-")
291
+ begin
292
+ warn "** flogging #{file}" if option[:verbose]
293
+ @parser = (option[:parser] || RubyParser).new
294
+
295
+ begin
296
+ ast = @parser.process(ruby, file)
297
+ rescue Timeout::Error
298
+ warn "TIMEOUT parsing #{file}. Skipping."
299
+ end
300
+
301
+ return unless ast
302
+ mass[file] = ast.mass
303
+ process ast
304
+ rescue RubyParser::SyntaxError, Racc::ParseError => e
305
+ q = option[:quiet]
306
+ if e.inspect =~ /<\%|%\>/ or ruby =~ /<\%|%\>/ then
307
+ return if q
308
+ warn "#{e.inspect} at #{e.backtrace.first(5).join(', ')}"
309
+ warn "\n...stupid lemmings and their bad erb templates... skipping"
310
+ else
311
+ warn "ERROR: parsing ruby file #{file}" unless q
312
+ unless option[:continue] then
313
+ warn "ERROR! Aborting. You may want to run with --continue."
314
+ raise e
315
+ end
316
+ return if q
317
+ warn "%s: %s at:\n %s" % [e.class, e.message.strip,
318
+ e.backtrace.first(5).join("\n ")]
319
+ end
320
+ end
321
+ end
322
+
323
+ ##
324
+ # Adds name to the class stack, for the duration of the block
325
+
326
+ def in_klass name
327
+ if Sexp === name then
328
+ name = case name.first
329
+ when :colon2 then
330
+ name = name.flatten
331
+ name.delete :const
332
+ name.delete :colon2
333
+ name.join("::")
334
+ when :colon3 then
335
+ name.last.to_s
336
+ else
337
+ raise "unknown type #{name.inspect}"
338
+ end
339
+ end
340
+
341
+ @class_stack.unshift name
342
+ yield
343
+ @class_stack.shift
344
+ end
345
+
346
+ ##
347
+ # Adds name to the method stack, for the duration of the block
348
+
349
+ def in_method(name, file, line)
350
+ method_name = Regexp === name ? name.inspect : name.to_s
351
+ @method_stack.unshift method_name
352
+ @method_locations[signature] = "#{file}:#{line}"
353
+ yield
354
+ @method_stack.shift
355
+ end
356
+
357
+ def initialize option = {}
358
+ super()
359
+ @option = option
360
+ @class_stack = []
361
+ @method_stack = []
362
+ @method_locations = {}
363
+ @mass = {}
364
+ @parser = nil
365
+ self.auto_shift_type = true
366
+ self.reset
367
+ end
368
+
369
+ ##
370
+ # Returns the first class in the list, or @@no_class if there are
371
+ # none.
372
+
373
+ def klass_name
374
+ name = @class_stack.first
375
+
376
+ if Sexp === name then
377
+ raise "you shouldn't see me"
378
+ elsif @class_stack.any?
379
+ @class_stack.reverse.join("::").sub(/\([^\)]+\)$/, '')
380
+ else
381
+ @@no_class
382
+ end
383
+ end
384
+
385
+ ##
386
+ # Returns the first method in the list, or "#none" if there are
387
+ # none.
388
+
389
+ def method_name
390
+ m = @method_stack.first || @@no_method
391
+ m = "##{m}" unless m =~ /::/
392
+ m
393
+ end
394
+
395
+ ##
396
+ # Output the report up to a given max or report everything, if nil.
397
+
398
+ def output_details io, max = nil
399
+ io.puts
400
+
401
+ each_by_score max do |class_method, score, call_list|
402
+ return 0 if option[:methods] and class_method =~ /##{@@no_method}/
403
+
404
+ self.print_score io, class_method, score
405
+
406
+ if option[:details] then
407
+ call_list.sort_by { |k,v| -v }.each do |call, count|
408
+ io.puts " %6.1f: %s" % [count, call]
409
+ end
410
+ io.puts
411
+ end
412
+ end
413
+ # io.puts
414
+ end
415
+
416
+ ##
417
+ # Output the report, grouped by class/module, up to a given max or
418
+ # report everything, if nil.
419
+
420
+ def output_details_grouped io, max = nil
421
+ scores = Hash.new 0
422
+ methods = Hash.new { |h,k| h[k] = [] }
423
+
424
+ each_by_score max do |class_method, score, call_list|
425
+ klass = class_method.split(/#|::/).first
426
+
427
+ methods[klass] << [class_method, score]
428
+ scores[klass] += score
429
+ end
430
+
431
+ scores.sort_by { |_, n| -n }.each do |klass, total|
432
+ io.puts
433
+
434
+ io.puts "%8.1f: %s" % [total, "#{klass} total"]
435
+
436
+ methods[klass].each do |name, score|
437
+ self.print_score io, name, score
438
+ end
439
+ end
440
+ end
441
+
442
+ ##
443
+ # For the duration of the block the complexity factor is increased
444
+ # by #bonus This allows the complexity of sub-expressions to be
445
+ # influenced by the expressions in which they are found. Yields 42
446
+ # to the supplied block.
447
+
448
+ def penalize_by bonus
449
+ @multiplier += bonus
450
+ yield
451
+ @multiplier -= bonus
452
+ end
453
+
454
+ ##
455
+ # Print out one formatted score.
456
+
457
+ def print_score io, name, score
458
+ location = @method_locations[name]
459
+ if location then
460
+ io.puts "%8.1f: %-32s %s" % [score, name, location]
461
+ else
462
+ io.puts "%8.1f: %s" % [score, name]
463
+ end
464
+ end
465
+
466
+ ##
467
+ # Process each element of #exp in turn.
468
+
469
+ def process_until_empty exp
470
+ process exp.shift until exp.empty?
471
+ end
472
+
473
+ ##
474
+ # Report results to #io, STDOUT by default.
475
+
476
+ def report(io = $stdout)
477
+ io.puts "%8.1f: %s" % [total, "flog total"]
478
+ io.puts "%8.1f: %s" % [average, "flog/method average"]
479
+
480
+ return if option[:score]
481
+
482
+ max = option[:all] ? nil : total * THRESHOLD
483
+ if option[:group] then
484
+ output_details_grouped io, max
485
+ else
486
+ output_details io, max
487
+ end
488
+ ensure
489
+ self.reset
490
+ end
491
+
492
+ ##
493
+ # Reset score data
494
+
495
+ def reset
496
+ @totals = @total_score = nil
497
+ @multiplier = 1.0
498
+ @calls = Hash.new { |h,k| h[k] = Hash.new 0 }
499
+ end
500
+
501
+ ##
502
+ # Compute the distance formula for a given tally
503
+
504
+ def score_method(tally)
505
+ a, b, c = 0, 0, 0
506
+ tally.each do |cat, score|
507
+ case cat
508
+ when :assignment then a += score
509
+ when :branch then b += score
510
+ else c += score
511
+ end
512
+ end
513
+ Math.sqrt(a*a + b*b + c*c)
514
+ end
515
+
516
+ def signature
517
+ "#{klass_name}#{method_name}"
518
+ end
519
+
520
+ def total # FIX: I hate this indirectness
521
+ totals unless @total_score # calculates total_score as well
522
+
523
+ @total_score
524
+ end
525
+
526
+ def max_score
527
+ max_method.last
528
+ end
529
+
530
+ def max_method
531
+ totals.max_by { |_, score| score }
532
+ end
533
+
534
+ ##
535
+ # Return the total score and populates @totals.
536
+
537
+ def totals
538
+ unless @totals then
539
+ @total_score = 0
540
+ @totals = Hash.new(0)
541
+
542
+ calls.each do |meth, tally|
543
+ next if option[:methods] and meth =~ /##{@@no_method}$/
544
+ score = score_method(tally)
545
+
546
+ @totals[meth] = score
547
+ @total_score += score
548
+ end
549
+ end
550
+
551
+ @totals
552
+ end
553
+
554
+ ############################################################
555
+ # Process Methods:
556
+
557
+ def process_alias(exp)
558
+ process exp.shift
559
+ process exp.shift
560
+ add_to_score :alias
561
+ s()
562
+ end
563
+
564
+ def process_and(exp)
565
+ add_to_score :branch
566
+ penalize_by 0.1 do
567
+ process exp.shift # lhs
568
+ process exp.shift # rhs
569
+ end
570
+ s()
571
+ end
572
+ alias :process_or :process_and
573
+
574
+ def process_attrasgn(exp)
575
+ add_to_score :assignment
576
+ process exp.shift # lhs
577
+ exp.shift # name
578
+ process_until_empty exp # rhs
579
+ s()
580
+ end
581
+
582
+ def process_block(exp)
583
+ penalize_by 0.1 do
584
+ process_until_empty exp
585
+ end
586
+ s()
587
+ end
588
+
589
+ def process_block_pass(exp)
590
+ arg = exp.shift
591
+
592
+ add_to_score :block_pass
593
+
594
+ case arg.first
595
+ when :lvar, :dvar, :ivar, :cvar, :self, :const, :colon2, :nil then
596
+ # do nothing
597
+ when :lit, :call then
598
+ add_to_score :to_proc_normal
599
+ when :lasgn then # blah(&l = proc { ... })
600
+ add_to_score :to_proc_lasgn
601
+ when :iter, :dsym, :dstr, *BRANCHING then
602
+ add_to_score :to_proc_icky!
603
+ else
604
+ raise({:block_pass_even_ickier! => arg}.inspect)
605
+ end
606
+
607
+ process arg
608
+
609
+ s()
610
+ end
611
+
612
+ def process_call(exp)
613
+ penalize_by 0.2 do
614
+ process exp.shift # recv
615
+ end
616
+
617
+ name = exp.shift
618
+
619
+ penalize_by 0.2 do
620
+ process_until_empty exp
621
+ end
622
+
623
+ add_to_score name, SCORES[name]
624
+
625
+ s()
626
+ end
627
+
628
+ def process_case(exp)
629
+ add_to_score :branch
630
+ process exp.shift # recv
631
+ penalize_by 0.1 do
632
+ process_until_empty exp
633
+ end
634
+ s()
635
+ end
636
+
637
+ def process_class(exp)
638
+ in_klass exp.shift do
639
+ penalize_by 1.0 do
640
+ process exp.shift # superclass expression
641
+ end
642
+ process_until_empty exp
643
+ end
644
+ s()
645
+ end
646
+
647
+ def process_dasgn_curr(exp) # FIX: remove
648
+ add_to_score :assignment
649
+ exp.shift # name
650
+ process exp.shift # assigment, if any
651
+ s()
652
+ end
653
+ alias :process_iasgn :process_dasgn_curr
654
+ alias :process_lasgn :process_dasgn_curr
655
+
656
+ def process_defn(exp)
657
+ in_method exp.shift, exp.file, exp.line do
658
+ process_until_empty exp
659
+ end
660
+ s()
661
+ end
662
+
663
+ def process_defs(exp)
664
+ process exp.shift # recv
665
+ in_method "::#{exp.shift}", exp.file, exp.line do
666
+ process_until_empty exp
667
+ end
668
+ s()
669
+ end
670
+
671
+ # TODO: it's not clear to me whether this can be generated at all.
672
+ def process_else(exp)
673
+ add_to_score :branch
674
+ penalize_by 0.1 do
675
+ process_until_empty exp
676
+ end
677
+ s()
678
+ end
679
+ alias :process_rescue :process_else
680
+ alias :process_when :process_else
681
+
682
+ def process_if(exp)
683
+ add_to_score :branch
684
+ process exp.shift # cond
685
+ penalize_by 0.1 do
686
+ process exp.shift # true
687
+ process exp.shift # false
688
+ end
689
+ s()
690
+ end
691
+
692
+ def dsl_name? args
693
+ return false unless args and not args.empty?
694
+
695
+ first_arg = args.first
696
+ first_arg = first_arg[1] if first_arg[0] == :hash
697
+
698
+ [:lit, :str].include? first_arg[0] and first_arg[1]
699
+ end
700
+
701
+ def process_iter(exp)
702
+ context = (self.context - [:class, :module, :scope])
703
+ context = context.uniq.sort_by { |s| s.to_s }
704
+
705
+ exp.delete 0 # { || ... } has 0 in arg slot
706
+
707
+ if context == [:block, :iter] or context == [:iter] then
708
+ recv = exp.first
709
+
710
+ # DSL w/ names. eg task :name do ... end
711
+ # looks like s(:call, nil, :task, s(:lit, :name))
712
+ # or s(:call, nil, :task, s(:str, "name"))
713
+ # or s(:call, nil, :task, s(:hash, s(:lit, :name) ...))
714
+
715
+ t, r, m, *a = recv
716
+
717
+ if t == :call and r == nil and submsg = dsl_name?(a) then
718
+ m = "#{m}(#{submsg})" if m and [String, Symbol].include?(submsg.class)
719
+ in_klass m do # :task/namespace
720
+ in_method submsg, exp.file, exp.line do # :name
721
+ process_until_empty exp
722
+ end
723
+ end
724
+ return s()
725
+ end
726
+ end
727
+
728
+ add_to_score :branch
729
+
730
+ process exp.shift # no penalty for LHS
731
+
732
+ penalize_by 0.1 do
733
+ process_until_empty exp
734
+ end
735
+
736
+ s()
737
+ end
738
+
739
+ def process_lit(exp)
740
+ value = exp.shift
741
+ case value
742
+ when 0, -1 then
743
+ # ignore those because they're used as array indicies instead of
744
+ # first/last
745
+ when Integer then
746
+ add_to_score :lit_fixnum
747
+ when Float, Symbol, Regexp, Range then
748
+ # do nothing
749
+ else
750
+ raise value.inspect
751
+ end
752
+ s()
753
+ end
754
+
755
+ def process_masgn(exp)
756
+ add_to_score :assignment
757
+
758
+ exp.map! { |s| Sexp === s ? s : s(:lasgn, s) }
759
+
760
+ process_until_empty exp
761
+ s()
762
+ end
763
+
764
+ def process_module(exp)
765
+ in_klass exp.shift do
766
+ process_until_empty exp
767
+ end
768
+ s()
769
+ end
770
+
771
+ def process_sclass(exp)
772
+ penalize_by 0.5 do
773
+ process exp.shift # recv
774
+ process_until_empty exp
775
+ end
776
+
777
+ add_to_score :sclass
778
+ s()
779
+ end
780
+
781
+ def process_super(exp)
782
+ add_to_score :super
783
+ process_until_empty exp
784
+ s()
785
+ end
786
+
787
+ def process_while(exp)
788
+ add_to_score :branch
789
+ penalize_by 0.1 do
790
+ process exp.shift # cond
791
+ process exp.shift # body
792
+ end
793
+ exp.shift # pre/post
794
+ s()
795
+ end
796
+ alias :process_until :process_while
797
+
798
+ def process_yield(exp)
799
+ add_to_score :yield
800
+ process_until_empty exp
801
+ s()
802
+ end
803
+ end