flay 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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