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 +0 -0
- data/History.txt +25 -0
- data/README.txt +9 -8
- data/Rakefile +19 -1
- data/lib/flay.rb +265 -17
- data/lib/flay_erb.rb +5 -0
- data/lib/flay_task.rb +22 -0
- data/lib/gauntlet_flay.rb +2 -0
- data/test/test_flay.rb +115 -37
- metadata +11 -11
- metadata.gz.sig +0 -0
data.tar.gz.sig
CHANGED
Binary file
|
data/History.txt
CHANGED
@@ -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
|
-
*
|
16
|
-
|
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
|
-
*
|
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
|
-
|
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
|
data/lib/flay.rb
CHANGED
@@ -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 =
|
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',
|
47
|
-
|
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
|
-
|
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 |
|
208
|
-
|
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|
|
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
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
466
|
+
extra = " (FUZZY)" if x.modified?
|
467
|
+
puts " #{c}: #{x.file}:#{x.line}#{extra}"
|
301
468
|
else
|
302
|
-
|
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
|
data/lib/flay_erb.rb
CHANGED
data/lib/flay_task.rb
CHANGED
@@ -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
|
data/lib/gauntlet_flay.rb
CHANGED
@@ -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:
|
data/test/test_flay.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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:
|
4
|
+
hash: 7
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 2.
|
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-
|
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:
|
79
|
+
hash: 21
|
80
80
|
segments:
|
81
81
|
- 4
|
82
|
-
-
|
83
|
-
version: "4.
|
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:
|
94
|
+
hash: 27
|
95
95
|
segments:
|
96
|
-
-
|
97
|
-
-
|
98
|
-
version: "
|
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
|