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 +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
|