flay 2.1.0 → 2.2.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.
data.tar.gz.sig CHANGED
Binary file
@@ -1,3 +1,28 @@
1
+ === 2.2.0 / 2013-04-09
2
+
3
+ Semantic versioning doesn't take into account how AWESOME a release
4
+ is. In this case, it severely falls short. I'd jump to 4.0 if I could.
5
+
6
+ * 2 major enhancements:
7
+
8
+ * Added --fuzzy (ie copy, paste, & modify) duplication detection.
9
+ * Added --liberal, which changes the way prune works to identify more duplication.
10
+
11
+ * 12 minor enhancements:
12
+
13
+ * Added -# to turn off item numbering. Helps with diffs to compare runs over time.
14
+ * Added Sexp#+.
15
+ * Added Sexp#code_index to specify where *code starts in some sexps.
16
+ * Added Sexp#has_code?.
17
+ * Added Sexp#initalize_copy to propagate file/line/modified info.
18
+ * Added Sexp#modified, #modified=, and #modified?.
19
+ * Added Sexp#split_at(n). (Something I've wanted in Array for ages).
20
+ * Added Sexp#split_code.
21
+ * Added mass and diff options to rake debug.
22
+ * Added rake run task w/ mass, diff, and liberal options
23
+ * Made report's sort more stable, so I can do better comparison runs.
24
+ * Wrapped Sexp#[] to propagate file/line/modified info.
25
+
1
26
  === 2.1.0 / 2013-02-13
2
27
 
3
28
  * 5 minor enhancements:
data/README.txt CHANGED
@@ -12,22 +12,23 @@ braces vs do/end, etc are all ignored. Making this totally rad.
12
12
 
13
13
  == FEATURES/PROBLEMS:
14
14
 
15
- * Plugin system allows other languages to be flayed.
16
- * Ships with .rb and .erb. javascript and others will be available separately.
17
- * Includes FlayTask for Rakefiles.
15
+ * Reports differences at any level of code.
16
+ * Adds a score multiplier to identical nodes.
18
17
  * Differences in literal values, variable, class, and method names are ignored.
19
18
  * Differences in whitespace, programming style, braces vs do/end, etc are ignored.
20
19
  * Works across files.
21
- * Reports differences at any level of code.
20
+ * Add the flay-persistent plugin to work across large/many projects.
21
+ * Run --diff to see an N-way diff of the code.
22
+ * Provides conservative (default) and --liberal pruning options.
23
+ * Provides --fuzzy duplication detection.
24
+ * Language independent: Plugin system allows other languages to be flayed.
25
+ * Ships with .rb and .erb. javascript and others will be available separately.
26
+ * Includes FlayTask for Rakefiles.
22
27
  * Totally rad.
23
- * Adds a score multiplier to identical nodes.
24
- * Run verbose to see an N-way diff of the code.
25
28
 
26
29
  == TODO:
27
30
 
28
31
  * Editor integration (emacs, textmate, other contributions welcome).
29
- * Score sequence fragments (a;b;c;d;e) vs (b;c;d) etc.
30
- * Persistent DB for efficient cross-project flaying.
31
32
 
32
33
  == SYNOPSIS:
33
34
 
data/Rakefile CHANGED
@@ -25,10 +25,28 @@ task :debug do
25
25
  require "flay"
26
26
 
27
27
  file = ENV["F"]
28
+ mass = ENV["M"]
29
+ diff = ENV["D"]
30
+ libr = ENV["L"]
28
31
 
29
- flay = Flay.new
32
+ opts = Flay.parse_options
33
+ opts[:mass] = mass.to_i if mass
34
+ opts[:diff] = diff.to_i if diff
35
+ opts[:liberal] = true if libr
36
+
37
+ flay = Flay.new opts
30
38
  flay.process(*Flay.expand_dirs_to_files(file))
31
39
  flay.report
32
40
  end
33
41
 
42
+ task :run do
43
+ file = ENV["F"]
44
+ fuzz = ENV["Z"] && "-f #{ENV["Z"]}"
45
+ mass = ENV["M"] && "-m #{ENV["M"]}"
46
+ diff = ENV["D"] && "-d"
47
+ libr = ENV["L"] && "-l"
48
+
49
+ ruby "#{Hoe::RUBY_FLAGS} bin/flay #{mass} #{fuzz} #{diff} #{libr} #{file}"
50
+ end
51
+
34
52
  # vim: syntax=ruby
@@ -7,7 +7,7 @@ require 'ruby_parser'
7
7
  require 'timeout'
8
8
 
9
9
  class File
10
- RUBY19 = "<3".respond_to? :encoding unless defined? RUBY19
10
+ RUBY19 = "<3".respond_to? :encoding unless defined? RUBY19 # :nodoc:
11
11
 
12
12
  class << self
13
13
  alias :binread :read unless RUBY19
@@ -15,7 +15,10 @@ class File
15
15
  end
16
16
 
17
17
  class Flay
18
- VERSION = '2.1.0'
18
+ VERSION = "2.2.0" # :nodoc:
19
+
20
+ ##
21
+ # Returns the default options.
19
22
 
20
23
  def self.default_options
21
24
  {
@@ -23,10 +26,16 @@ class Flay
23
26
  :mass => 16,
24
27
  :summary => false,
25
28
  :verbose => false,
29
+ :number => true,
26
30
  :timeout => 10,
31
+ :liberal => false,
32
+ :fuzzy => false,
27
33
  }
28
34
  end
29
35
 
36
+ ##
37
+ # Process options in +args+, defaulting to +ARGV+.
38
+
30
39
  def self.parse_options args = ARGV
31
40
  options = self.default_options
32
41
 
@@ -43,8 +52,13 @@ class Flay
43
52
  exit
44
53
  end
45
54
 
46
- opts.on('-f', '--fuzzy', "DEAD: fuzzy similarities.") do
47
- abort "--fuzzy is no longer supported. Sorry. It sucked."
55
+ opts.on('-f', '--fuzzy [DIFF]', Integer,
56
+ "Detect fuzzy (copy & paste) duplication (default 1).") do |n|
57
+ options[:fuzzy] = n || 1
58
+ end
59
+
60
+ opts.on('-l', '--liberal', "Use a more liberal detection method.") do
61
+ options[:liberal] = true
48
62
  end
49
63
 
50
64
  opts.on('-m', '--mass MASS', Integer,
@@ -52,6 +66,10 @@ class Flay
52
66
  options[:mass] = m.to_i
53
67
  end
54
68
 
69
+ opts.on('-#', "Don't number output (helps with diffs)") do |m|
70
+ options[:number] = false
71
+ end
72
+
55
73
  opts.on('-v', '--verbose', "Verbose. Show progress processing files.") do
56
74
  options[:verbose] = true
57
75
  end
@@ -89,6 +107,11 @@ class Flay
89
107
  options
90
108
  end
91
109
 
110
+ ##
111
+ # Expands +*dirs+ to all files within that match ruby and rake extensions.
112
+ # --
113
+ # REFACTOR: from flog
114
+
92
115
  def self.expand_dirs_to_files *dirs
93
116
  extensions = ['rb'] + Flay.load_plugins
94
117
 
@@ -101,6 +124,9 @@ class Flay
101
124
  }.flatten
102
125
  end
103
126
 
127
+ ##
128
+ # Loads all flay plugins. Files must be named "flog_*.rb".
129
+
104
130
  def self.load_plugins
105
131
  unless defined? @@plugins then
106
132
  @@plugins = []
@@ -123,8 +149,13 @@ class Flay
123
149
  # ignore
124
150
  end
125
151
 
152
+ # :stopdoc:
126
153
  attr_accessor :mass_threshold, :total, :identical, :masses
127
154
  attr_reader :hashes, :option
155
+ # :startdoc:
156
+
157
+ ##
158
+ # Create a new instance of Flay with +option+s.
128
159
 
129
160
  def initialize option = nil
130
161
  @option = option || Flay.default_options
@@ -138,6 +169,9 @@ class Flay
138
169
  require 'ruby2ruby' if @option[:diff]
139
170
  end
140
171
 
172
+ ##
173
+ # Process any number of files.
174
+
141
175
  def process(*files) # TODO: rename from process - should act as SexpProcessor
142
176
  files.each do |file|
143
177
  warn "Processing #{file}" if option[:verbose]
@@ -169,17 +203,38 @@ class Flay
169
203
  end
170
204
  end
171
205
 
206
+ ##
207
+ # Prune, find identical nodes, and update masses.
208
+
172
209
  def analyze
173
210
  self.prune
174
211
 
175
212
  self.hashes.each do |hash,nodes|
176
213
  identical[hash] = nodes[1..-1].all? { |n| n == nodes.first }
214
+ end
215
+
216
+ update_masses
217
+ end
218
+
219
+ ##
220
+ # Reset total and recalculate the masses for all nodes in +hashes+.
221
+
222
+ def update_masses
223
+ self.total = 0
224
+ masses.clear
225
+ self.hashes.each do |hash, nodes|
177
226
  masses[hash] = nodes.first.mass * nodes.size
178
227
  masses[hash] *= (nodes.size) if identical[hash]
179
228
  self.total += masses[hash]
180
229
  end
181
230
  end
182
231
 
232
+ ##
233
+ # Parse a ruby +file+ and return the sexp.
234
+ #
235
+ # --
236
+ # TODO: change the system and rename this to parse_rb.
237
+
183
238
  def process_rb file
184
239
  begin
185
240
  RubyParser.new.process(File.binread(file), file, option[:timeout])
@@ -188,26 +243,80 @@ class Flay
188
243
  end
189
244
  end
190
245
 
246
+ ##
247
+ # Process a sexp +pt+.
248
+
191
249
  def process_sexp pt
192
250
  pt.deep_each do |node|
193
251
  next unless node.any? { |sub| Sexp === sub }
194
252
  next if node.mass < self.mass_threshold
195
253
 
196
254
  self.hashes[node.structural_hash] << node
255
+
256
+ process_fuzzy node, option[:fuzzy] if option[:fuzzy]
257
+ end
258
+ end
259
+
260
+ # :stopdoc:
261
+ MAX_NODE_SIZE = 10 # prevents exponential blowout
262
+ MAX_AVG_MASS = 12 # prevents exponential blowout
263
+ # :startdoc:
264
+
265
+ ##
266
+ # Process "fuzzy" matches for +node+. A fuzzy match is a subset of
267
+ # +node+ up to +difference+ elements less than the original.
268
+
269
+ def process_fuzzy node, difference
270
+ return unless node.has_code?
271
+
272
+ avg_mass = node.mass / node.size
273
+ return if node.size > MAX_NODE_SIZE or avg_mass > MAX_AVG_MASS
274
+
275
+ tmpl, code = node.split_code
276
+ tmpl.modified = true
277
+
278
+ (code.size - 1).downto(code.size - difference) do |n|
279
+ code.combination(n).each do |subcode|
280
+ new_node = tmpl + subcode
281
+
282
+ next unless new_node.any? { |sub| Sexp === sub }
283
+ next if new_node.mass < self.mass_threshold
284
+
285
+ # they're already structurally similar, don't bother adding another
286
+ next if self.hashes[new_node.structural_hash].any? { |sub|
287
+ sub.file == new_node.file and sub.line == new_node.line
288
+ }
289
+
290
+ self.hashes[new_node.structural_hash] << new_node
291
+ end
197
292
  end
198
293
  end
199
294
 
295
+ ##
296
+ # Prunes nodes that aren't relevant to analysis or are already
297
+ # covered by another node.
298
+
200
299
  def prune
201
300
  # prune trees that aren't duped at all, or are too small
202
301
  self.hashes.delete_if { |_,nodes| nodes.size == 1 }
302
+ self.hashes.delete_if { |_,nodes| nodes.all?(&:modified?) }
203
303
 
204
- # extract all subtree hashes from all nodes
304
+ return prune_liberally if option[:liberal]
305
+
306
+ prune_conservatively
307
+ end
308
+
309
+ ##
310
+ # Conservative prune. Remove any bucket that is known to contain a
311
+ # subnode element of a node in another bucket.
312
+
313
+ def prune_conservatively
205
314
  all_hashes = {}
315
+
316
+ # extract all subtree hashes from all nodes
206
317
  self.hashes.values.each do |nodes|
207
- nodes.each do |node|
208
- node.all_structural_subhashes.each do |h|
209
- all_hashes[h] = true
210
- end
318
+ nodes.first.all_structural_subhashes.each do |h|
319
+ all_hashes[h] = true
211
320
  end
212
321
  end
213
322
 
@@ -215,6 +324,45 @@ class Flay
215
324
  self.hashes.delete_if { |h,_| all_hashes[h] }
216
325
  end
217
326
 
327
+ ##
328
+ # Liberal prune. Remove any _element_ from a bucket that is known to
329
+ # be a subnode of another node. Removed by identity.
330
+
331
+ def prune_liberally
332
+ update_masses
333
+
334
+ all_hashes = Hash.new { |h,k| h[k] = [] }
335
+
336
+ # record each subtree by subhash, but skip if subtree mass > parent mass
337
+ self.hashes.values.each do |nodes|
338
+ nodes.each do |node|
339
+ tophash = node.structural_hash
340
+ topscore = self.masses[tophash]
341
+
342
+ node.deep_each do |subnode|
343
+ subhash = subnode.structural_hash
344
+ subscore = self.masses[subhash]
345
+
346
+ next if subscore and subscore > topscore
347
+
348
+ all_hashes[subhash] << subnode
349
+ end
350
+ end
351
+ end
352
+
353
+ # nuke only individual items by object identity
354
+ self.hashes.each do |h,v|
355
+ v.delete_eql all_hashes[h]
356
+ end
357
+
358
+ # nuke buckets we happened to fully empty
359
+ self.hashes.delete_if { |k,v| v.size <= 1 }
360
+ end
361
+
362
+ ##
363
+ # Output an n-way diff from +data+. This is only used if --diff is
364
+ # given.
365
+
218
366
  def n_way_diff *data
219
367
  data.each_with_index do |s, i|
220
368
  c = (?A.ord + i).chr
@@ -246,6 +394,9 @@ class Flay
246
394
  groups.flatten.join("\n")
247
395
  end
248
396
 
397
+ ##
398
+ # Calculate summary scores on a per-file basis. For --summary.
399
+
249
400
  def summary
250
401
  score = Hash.new 0
251
402
 
@@ -260,13 +411,16 @@ class Flay
260
411
  score
261
412
  end
262
413
 
414
+ ##
415
+ # Output the report. Duh.
416
+
263
417
  def report prune = nil
264
418
  analyze
265
419
 
266
420
  puts "Total score (lower is better) = #{self.total}"
267
- puts
268
421
 
269
422
  if option[:summary] then
423
+ puts
270
424
 
271
425
  self.summary.sort_by { |_,v| -v }.each do |file, score|
272
426
  puts "%8.2f: %s" % [score, file]
@@ -276,7 +430,13 @@ class Flay
276
430
  end
277
431
 
278
432
  count = 0
279
- masses.sort_by { |h,m| [-m, hashes[h].first.file] }.each do |hash, mass|
433
+ sorted = masses.sort_by { |h,m|
434
+ [-m,
435
+ hashes[h].first.file,
436
+ hashes[h].first.line,
437
+ hashes[h].first.first.to_s]
438
+ }
439
+ sorted.each do |hash, mass|
280
440
  nodes = hashes[hash]
281
441
  next unless nodes.first.first == prune if prune
282
442
  puts
@@ -290,16 +450,24 @@ class Flay
290
450
  ["Similar", ""]
291
451
  end
292
452
 
293
- count += 1
294
- puts "%d) %s code found in %p (mass%s = %d)" %
295
- [count, match, node.first, bonus, mass]
453
+ if option[:number] then
454
+ count += 1
455
+
456
+ puts "%d) %s code found in %p (mass%s = %d)" %
457
+ [count, match, node.first, bonus, mass]
458
+ else
459
+ puts "%s code found in %p (mass%s = %d)" %
460
+ [match, node.first, bonus, mass]
461
+ end
296
462
 
297
463
  nodes.sort_by { |x| [x.file, x.line] }.each_with_index do |x, i|
298
464
  if option[:diff] then
299
465
  c = (?A.ord + i).chr
300
- puts " #{c}: #{x.file}:#{x.line}"
466
+ extra = " (FUZZY)" if x.modified?
467
+ puts " #{c}: #{x.file}:#{x.line}#{extra}"
301
468
  else
302
- puts " #{x.file}:#{x.line}"
469
+ extra = " (FUZZY)" if x.modified?
470
+ puts " #{x.file}:#{x.line}#{extra}"
303
471
  end
304
472
  end
305
473
 
@@ -313,14 +481,28 @@ class Flay
313
481
  end
314
482
 
315
483
  class String
316
- attr_accessor :group
484
+ attr_accessor :group # :nodoc:
317
485
  end
318
486
 
319
487
  class Sexp
488
+ ##
489
+ # Whether or not this sexp is a mutated/modified sexp.
490
+
491
+ attr_accessor :modified
492
+ alias :modified? :modified # Is this sexp modified?
493
+
494
+ ##
495
+ # Calculate the structural hash for this sexp. Cached, so don't
496
+ # modify the sexp afterwards and expect it to be correct.
497
+
320
498
  def structural_hash
321
499
  @structural_hash ||= self.structure.hash
322
500
  end
323
501
 
502
+ ##
503
+ # Returns a list of structural hashes for all nodes (and sub-nodes)
504
+ # of this sexp.
505
+
324
506
  def all_structural_subhashes
325
507
  hashes = []
326
508
  self.deep_each do |node|
@@ -328,4 +510,70 @@ class Sexp
328
510
  end
329
511
  hashes
330
512
  end
513
+
514
+ def initialize_copy o # :nodoc:
515
+ s = super
516
+ s.file = o.file
517
+ s.line = o.line
518
+ s.modified = o.modified
519
+ s
520
+ end
521
+
522
+ def [] a # :nodoc:
523
+ s = super
524
+ if Sexp === s then
525
+ s.file = self.file
526
+ s.line = self.line
527
+ s.modified = self.modified
528
+ end
529
+ s
530
+ end
531
+
532
+ def + o # :nodoc:
533
+ self.dup.concat o
534
+ end
535
+
536
+ ##
537
+ # Useful general array method that splits the array from 0..+n+ and
538
+ # the rest. Returns both sections.
539
+
540
+ def split_at n
541
+ return self[0..n], self[n+1..-1]
542
+ end
543
+
544
+ ##
545
+ # Return the index of the last non-code element, or nil if this sexp
546
+ # is not a code-bearing node.
547
+
548
+ def code_index
549
+ {
550
+ :block => 0, # s(:block, *code)
551
+ :class => 2, # s(:class, name, super, *code)
552
+ :module => 1, # s(:module, name, *code)
553
+ :defn => 2, # s(:defn, name, args, *code)
554
+ :defs => 3, # s(:defs, recv, name, args, *code)
555
+ :iter => 2, # s(:iter, recv, args, *code)
556
+ }[self.sexp_type]
557
+ end
558
+
559
+ alias has_code? code_index # Does this sexp have a +*code+ section?
560
+
561
+ ##
562
+ # Split the sexp into front-matter and code-matter, returning both.
563
+ # See #code_index.
564
+
565
+ def split_code
566
+ index = self.code_index
567
+ self.split_at index if index
568
+ end
569
+ end
570
+
571
+ class Array # :nodoc:
572
+
573
+ ##
574
+ # Delete anything in +self+ if they are identical to anything in +other+.
575
+
576
+ def delete_eql other
577
+ self.delete_if { |o1| other.any? { |o2| o1.equal? o2 } }
578
+ end
331
579
  end
@@ -5,6 +5,11 @@ require 'flay'
5
5
  require 'erb'
6
6
 
7
7
  class Flay
8
+
9
+ ##
10
+ # Process erb and parse the result. Returns the sexp of the parsed
11
+ # ruby.
12
+
8
13
  def process_erb file
9
14
  erb = File.read file
10
15
 
@@ -1,9 +1,28 @@
1
1
  class FlayTask < Rake::TaskLib
2
+ ##
3
+ # The name of the task. Defaults to :flay
4
+
2
5
  attr_accessor :name
6
+
7
+ ##
8
+ # What directories to operate on. Sensible defaults.
9
+
3
10
  attr_accessor :dirs
11
+
12
+ ##
13
+ # Threshold to fail the task at. Default 200.
14
+
4
15
  attr_accessor :threshold
16
+
17
+ ##
18
+ # Verbosity of output. Defaults to rake's trace (-t) option.
19
+
5
20
  attr_accessor :verbose
6
21
 
22
+ ##
23
+ # Creates a new FlayTask instance with given +name+, +threshold+,
24
+ # and +dirs+.
25
+
7
26
  def initialize name = :flay, threshold = 200, dirs = nil
8
27
  @name = name
9
28
  @dirs = dirs || %w(app bin lib spec test)
@@ -17,6 +36,9 @@ class FlayTask < Rake::TaskLib
17
36
  define
18
37
  end
19
38
 
39
+ ##
40
+ # Defines the flay task.
41
+
20
42
  def define
21
43
  desc "Analyze for code duplication in: #{dirs.join(', ')}"
22
44
  task name do
@@ -10,6 +10,7 @@ require 'flay'
10
10
  require 'gauntlet'
11
11
  require 'pp'
12
12
 
13
+ # :stopdoc:
13
14
  class FlayGauntlet < Gauntlet
14
15
  $owners = {}
15
16
  $score_file = 'flay-scores.yml'
@@ -98,3 +99,4 @@ filter = Regexp.new filter if filter
98
99
  flayer = FlayGauntlet.new
99
100
  flayer.run_the_gauntlet filter
100
101
  flayer.display_report max
102
+ # :startdoc:
@@ -24,6 +24,20 @@ class TestSexp < MiniTest::Unit::TestCase
24
24
  assert_equal hash, @s.deep_clone.structural_hash
25
25
  end
26
26
 
27
+ def test_delete_eql
28
+ s1 = s(:a, s(:b, s(:c)))
29
+ s2 = s(:a, s(:b, s(:c)))
30
+ s3 = s(:a, s(:b, s(:c)))
31
+
32
+ a1 = [s1, s2, s3]
33
+ a2 = [s1, s3]
34
+
35
+ a1.delete_eql a2
36
+
37
+ assert_equal [s2], a1
38
+ assert_same s2, a1.first
39
+ end
40
+
27
41
  def test_all_structural_subhashes
28
42
  s = s(:iter,
29
43
  s(:call, s(:arglist, s(:lit))),
@@ -50,22 +64,110 @@ class TestSexp < MiniTest::Unit::TestCase
50
64
  assert_equal expected, x.sort.uniq
51
65
  end
52
66
 
67
+ DOG_AND_CAT = Ruby18Parser.new.process <<-RUBY
68
+ class Dog
69
+ def x
70
+ return "Hello"
71
+ end
72
+ end
73
+ class Cat
74
+ def y
75
+ return "Hello"
76
+ end
77
+ end
78
+ RUBY
79
+
80
+ ROUND = Ruby18Parser.new.process <<-RUBY
81
+ def x(n)
82
+ if n % 2 == 0
83
+ return n
84
+ else
85
+ return n + 1
86
+ end
87
+ end
88
+ RUBY
89
+
90
+ def test_prune
91
+ contained = s(:a, s(:b,s(:c)), s(:d,s(:e)))
92
+ container = s(:d, contained)
93
+
94
+ flay = Flay.new :mass => 0
95
+ flay.process_sexp s(:outer,contained)
96
+ 2.times { flay.process_sexp s(:outer,container) }
97
+
98
+ exp = eval <<-EOM # just to prevent emacs from reindenting it
99
+ [
100
+ [ s(:a, s(:b, s(:c)), s(:d, s(:e))),
101
+ s(:a, s(:b, s(:c)), s(:d, s(:e))),
102
+ s(:a, s(:b, s(:c)), s(:d, s(:e)))],
103
+ [ s(:b, s(:c)),
104
+ s(:b, s(:c)),
105
+ s(:b, s(:c))],
106
+ [s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e)))),
107
+ s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e))))],
108
+ [ s(:d, s(:e)),
109
+ s(:d, s(:e)),
110
+ s(:d, s(:e))],
111
+ ]
112
+ EOM
113
+
114
+ assert_equal exp, flay.hashes.values.sort_by(&:inspect)
115
+
116
+ flay.prune
117
+
118
+ exp = [
119
+ [s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e)))),
120
+ s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e))))]
121
+ ]
122
+
123
+ assert_equal exp, flay.hashes.values.sort_by(&:inspect)
124
+ end
125
+
126
+ def test_prune_liberal
127
+ contained = s(:a, s(:b,s(:c)), s(:d,s(:e)))
128
+ container = s(:d, contained)
129
+
130
+ flay = Flay.new :mass => 0, :liberal => true
131
+ flay.process_sexp s(:outer,contained)
132
+ 2.times { flay.process_sexp s(:outer,container) }
133
+
134
+ exp = eval <<-EOM # just to prevent emacs from reindenting it
135
+ [
136
+ [ s(:a, s(:b, s(:c)), s(:d, s(:e))),
137
+ s(:a, s(:b, s(:c)), s(:d, s(:e))),
138
+ s(:a, s(:b, s(:c)), s(:d, s(:e)))],
139
+ [ s(:b, s(:c)),
140
+ s(:b, s(:c)),
141
+ s(:b, s(:c))],
142
+ [s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e)))),
143
+ s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e))))],
144
+ [ s(:d, s(:e)),
145
+ s(:d, s(:e)),
146
+ s(:d, s(:e))],
147
+ ]
148
+ EOM
149
+
150
+ assert_equal exp, flay.hashes.values.sort_by(&:inspect)
151
+
152
+ flay.prune
153
+
154
+ exp = [
155
+ [s(:a, s(:b, s(:c)), s(:d, s(:e))),
156
+ s(:a, s(:b, s(:c)), s(:d, s(:e))),
157
+ s(:a, s(:b, s(:c)), s(:d, s(:e)))],
158
+ [s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e)))),
159
+ s(:d, s(:a, s(:b, s(:c)), s(:d, s(:e))))]
160
+ ]
161
+
162
+ assert_equal exp, flay.hashes.values.sort_by(&:inspect)
163
+ end
164
+
53
165
  def test_process_sexp
54
166
  flay = Flay.new
55
167
 
56
- s = Ruby18Parser.new.process <<-RUBY
57
- def x(n)
58
- if n % 2 == 0
59
- return n
60
- else
61
- return n + 1
62
- end
63
- end
64
- RUBY
65
-
66
168
  expected = [] # only ones big enough
67
169
 
68
- flay.process_sexp s
170
+ flay.process_sexp ROUND.deep_clone
69
171
 
70
172
  actual = flay.hashes.values.map { |sexps| sexps.map { |sexp| sexp.first } }
71
173
 
@@ -75,23 +177,13 @@ class TestSexp < MiniTest::Unit::TestCase
75
177
  def test_process_sexp_full
76
178
  flay = Flay.new(:mass => 1)
77
179
 
78
- s = Ruby18Parser.new.process <<-RUBY
79
- def x(n)
80
- if n % 2 == 0
81
- return n
82
- else
83
- return n + 1
84
- end
85
- end
86
- RUBY
87
-
88
180
  expected = [[:call, :call],
89
181
  [:call],
90
182
  [:if],
91
183
  [:return],
92
184
  [:return]]
93
185
 
94
- flay.process_sexp s
186
+ flay.process_sexp ROUND.deep_clone
95
187
 
96
188
  actual = flay.hashes.values.map { |sexps| sexps.map { |sexp| sexp.first } }
97
189
 
@@ -119,20 +211,7 @@ class TestSexp < MiniTest::Unit::TestCase
119
211
 
120
212
  flay = Flay.new opts
121
213
 
122
- s = Ruby18Parser.new.process <<-RUBY
123
- class Dog
124
- def x
125
- return "Hello"
126
- end
127
- end
128
- class Cat
129
- def y
130
- return "Hello"
131
- end
132
- end
133
- RUBY
134
-
135
- flay.process_sexp s
214
+ flay.process_sexp DOG_AND_CAT.deep_clone
136
215
  flay.analyze
137
216
 
138
217
  out, err = capture_io do
@@ -142,7 +221,6 @@ class TestSexp < MiniTest::Unit::TestCase
142
221
  exp = <<-END.gsub(/\d+/, "N").gsub(/^ {6}/, "")
143
222
  Total score (lower is better) = 16
144
223
 
145
-
146
224
  1) Similar code found in :class (mass = 16)
147
225
  A: (string):1
148
226
  B: (string):6
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flay
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
7
  - 2
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 2.1.0
10
+ version: 2.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ryan Davis
@@ -36,7 +36,7 @@ cert_chain:
36
36
  FBHgymkyj/AOSqKRIpXPhjC6
37
37
  -----END CERTIFICATE-----
38
38
 
39
- date: 2013-02-14 00:00:00 Z
39
+ date: 2013-04-10 00:00:00 Z
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: sexp_processor
@@ -76,11 +76,11 @@ dependencies:
76
76
  requirements:
77
77
  - - ~>
78
78
  - !ruby/object:Gem::Version
79
- hash: 23
79
+ hash: 21
80
80
  segments:
81
81
  - 4
82
- - 6
83
- version: "4.6"
82
+ - 7
83
+ version: "4.7"
84
84
  type: :development
85
85
  version_requirements: *id003
86
86
  - !ruby/object:Gem::Dependency
@@ -91,11 +91,11 @@ dependencies:
91
91
  requirements:
92
92
  - - ~>
93
93
  - !ruby/object:Gem::Version
94
- hash: 19
94
+ hash: 27
95
95
  segments:
96
- - 3
97
- - 10
98
- version: "3.10"
96
+ - 4
97
+ - 0
98
+ version: "4.0"
99
99
  type: :development
100
100
  version_requirements: *id004
101
101
  - !ruby/object:Gem::Dependency
metadata.gz.sig CHANGED
Binary file