mmullis-flay 1.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +80 -0
- data/Manifest.txt +10 -0
- data/README.txt +102 -0
- data/Rakefile +22 -0
- data/VERSION +1 -0
- data/bin/flay +11 -0
- data/lib/flay.rb +326 -0
- data/lib/flay_erb.rb +19 -0
- data/lib/flay_task.rb +33 -0
- data/lib/gauntlet_flay.rb +100 -0
- data/test/test_flay.rb +139 -0
- metadata +89 -0
data/History.txt
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
=== 1.4.2 / 2011-02-18
|
2
|
+
|
3
|
+
* 2 bug fixes:
|
4
|
+
|
5
|
+
* Added flay require in flay_task
|
6
|
+
* Switched to minitest. (doh)
|
7
|
+
|
8
|
+
=== 1.4.1 / 2010-09-01
|
9
|
+
|
10
|
+
* 2 minor enhancements:
|
11
|
+
|
12
|
+
* Added extra error handling for ERB flay to deal with tons of bad ERB
|
13
|
+
* Skip plugin if another version already loaded (eg local vs gem).
|
14
|
+
|
15
|
+
* 1 bug fix:
|
16
|
+
|
17
|
+
* Fixed all tests that were having problems on 1.9 due to unstable hashes
|
18
|
+
|
19
|
+
=== 1.4.0 / 2009-08-14
|
20
|
+
|
21
|
+
* 4 minor enhancements:
|
22
|
+
|
23
|
+
* Pushed Sexp#mass up to sexp_processor.
|
24
|
+
* Removed #similarity #compare_to, #intersection, #triangle, and other cruft.
|
25
|
+
* Renamed all_subhashes to all_structural_subhashes.
|
26
|
+
* Renamed fuzzy_hash to structural_hash.
|
27
|
+
|
28
|
+
=== 1.3.0 / 2009-06-23
|
29
|
+
|
30
|
+
* 5 minor enhancements:
|
31
|
+
|
32
|
+
* Added --summary to display flay scores per file.
|
33
|
+
* Added --verbose to display processing progress.
|
34
|
+
* Protect against syntax errors in bad code and continue flaying.
|
35
|
+
* Removed fuzzy matching. Never got it to feel right. Slow. Broken on 1.9
|
36
|
+
* Renamed --verbose to --diff.
|
37
|
+
|
38
|
+
=== 1.2.1 / 2009-03-16
|
39
|
+
|
40
|
+
* 3 minor enhancements:
|
41
|
+
|
42
|
+
* Added gauntlet_flay.rb
|
43
|
+
* Cached value of plugins loaded.
|
44
|
+
* Refactored and separated analysis phase from process phase
|
45
|
+
|
46
|
+
* 1 bug fix:
|
47
|
+
|
48
|
+
* Added bin dir to default dirs list in FlayTask
|
49
|
+
|
50
|
+
=== 1.2.0 / 2009-03-09
|
51
|
+
|
52
|
+
* 2 major enhancements:
|
53
|
+
|
54
|
+
* Added flay_task.rb
|
55
|
+
* Added plugin system (any flay_(c,java,js,etc).rb files).
|
56
|
+
|
57
|
+
* 4 minor enhancements:
|
58
|
+
|
59
|
+
* Added expand_dirs_to_files and made dirs valid arguments.
|
60
|
+
* Added flay_erb.rb plugin.
|
61
|
+
* Added optparse option processing.
|
62
|
+
* Refactored to make using w/in rake and other CI systems clean and easy.
|
63
|
+
|
64
|
+
=== 1.1.0 / 2009-01-20
|
65
|
+
|
66
|
+
* 7 minor enhancement:
|
67
|
+
|
68
|
+
* Added -v verbose mode to print out N-way diff of the detected code.
|
69
|
+
* Added identical node scoring and reporting.
|
70
|
+
* Added the start of copy/paste+edit detection, not even close yet.
|
71
|
+
* Added more tests.
|
72
|
+
* Added rcov tasks
|
73
|
+
* Clarified output a bit
|
74
|
+
* Refactored process_sexps to make doing other languages/systems easier.
|
75
|
+
|
76
|
+
=== 1.0.0 / 2008-11-06
|
77
|
+
|
78
|
+
* 1 major enhancement
|
79
|
+
|
80
|
+
* Birthday!
|
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
= flay
|
2
|
+
|
3
|
+
* http://ruby.sadi.st/
|
4
|
+
* http://rubyforge.org/projects/seattlerb
|
5
|
+
|
6
|
+
== DESCRIPTION:
|
7
|
+
|
8
|
+
Flay analyzes code for structural similarities. Differences in literal
|
9
|
+
values, variable, class, method names, whitespace, programming style,
|
10
|
+
braces vs do/end, etc are all ignored. Making this totally rad.
|
11
|
+
|
12
|
+
== FEATURES/PROBLEMS:
|
13
|
+
|
14
|
+
* Plugin system allows other languages to be flayed.
|
15
|
+
* Ships with .rb and .erb. javascript and others will be available separately.
|
16
|
+
* Includes FlayTask for Rakefiles.
|
17
|
+
* Differences in literal values, variable, class, and method names are ignored.
|
18
|
+
* Differences in whitespace, programming style, braces vs do/end, etc are ignored.
|
19
|
+
* Works across files.
|
20
|
+
* Reports differences at any level of code.
|
21
|
+
* Totally rad.
|
22
|
+
* Adds a score multiplier to identical nodes.
|
23
|
+
* Run verbose to see an N-way diff of the code.
|
24
|
+
|
25
|
+
== TODO:
|
26
|
+
|
27
|
+
* Editor integration (emacs, textmate, other contributions welcome).
|
28
|
+
* Score sequence fragments (a;b;c;d;e) vs (b;c;d) etc.
|
29
|
+
|
30
|
+
== SYNOPSIS:
|
31
|
+
|
32
|
+
% flay -v ~/Work/svn/ruby/ruby_1_8/lib/cgi.rb
|
33
|
+
Processing /Users/ryan/Work/svn/ruby/ruby_1_8/lib/cgi.rb...
|
34
|
+
|
35
|
+
Matches found in :defn (mass = 184)
|
36
|
+
A: /Users/ryan/Work/svn/ruby/ruby_1_8/lib/cgi.rb:1470
|
37
|
+
B: /Users/ryan/Work/svn/ruby/ruby_1_8/lib/cgi.rb:1925
|
38
|
+
|
39
|
+
A: def checkbox_group(name = "", *values)
|
40
|
+
B: def radio_group(name = "", *values)
|
41
|
+
if name.kind_of?(Hash) then
|
42
|
+
values = name["VALUES"]
|
43
|
+
name = name["NAME"]
|
44
|
+
end
|
45
|
+
values.collect do |value|
|
46
|
+
if value.kind_of?(String) then
|
47
|
+
A: (checkbox(name, value) + value)
|
48
|
+
B: (radio_button(name, value) + value)
|
49
|
+
else
|
50
|
+
if (value[(value.size - 1)] == true) then
|
51
|
+
A: (checkbox(name, value[0], true) + value[(value.size - 2)])
|
52
|
+
B: (radio_button(name, value[0], true) + value[(value.size - 2)])
|
53
|
+
else
|
54
|
+
A: (checkbox(name, value[0]) + value[(value.size - 1)])
|
55
|
+
B: (radio_button(name, value[0]) + value[(value.size - 1)])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
IDENTICAL Matches found in :for (mass*2 = 144)
|
62
|
+
A: /Users/ryan/Work/svn/ruby/ruby_1_8/lib/cgi.rb:2160
|
63
|
+
B: /Users/ryan/Work/svn/ruby/ruby_1_8/lib/cgi.rb:2217
|
64
|
+
|
65
|
+
for element in ["HTML", "BODY", "P", "DT", "DD", "LI", "OPTION", "THEAD", "TFOOT", "TBODY", "COLGROUP", "TR", "TH", "TD", "HEAD"] do
|
66
|
+
methods = (methods + ((" def #{element.downcase}(attributes = {})\n" + nO_element_def(element)) + " end\n"))
|
67
|
+
end
|
68
|
+
...
|
69
|
+
|
70
|
+
== REQUIREMENTS:
|
71
|
+
|
72
|
+
* ruby_parser
|
73
|
+
* sexp_processor
|
74
|
+
|
75
|
+
== INSTALL:
|
76
|
+
|
77
|
+
* sudo gem install flay
|
78
|
+
|
79
|
+
== LICENSE:
|
80
|
+
|
81
|
+
(The MIT License)
|
82
|
+
|
83
|
+
Copyright (c) 2008-2009 Ryan Davis, Seattle.rb
|
84
|
+
|
85
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
86
|
+
a copy of this software and associated documentation files (the
|
87
|
+
'Software'), to deal in the Software without restriction, including
|
88
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
89
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
90
|
+
permit persons to whom the Software is furnished to do so, subject to
|
91
|
+
the following conditions:
|
92
|
+
|
93
|
+
The above copyright notice and this permission notice shall be
|
94
|
+
included in all copies or substantial portions of the Software.
|
95
|
+
|
96
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
97
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
98
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
99
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
100
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
101
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
102
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
|
6
|
+
Hoe::add_include_dirs(".",
|
7
|
+
"../../sexp_processor/dev/lib",
|
8
|
+
"../../ruby_parser/dev/lib")
|
9
|
+
|
10
|
+
Hoe.plugin :seattlerb
|
11
|
+
|
12
|
+
Hoe.spec 'flay' do
|
13
|
+
developer 'Ryan Davis', 'ryand-ruby@zenspider.com'
|
14
|
+
|
15
|
+
self.rubyforge_name = 'seattlerb'
|
16
|
+
self.flay_threshold = 250
|
17
|
+
|
18
|
+
extra_deps << ['sexp_processor', '~> 3.0']
|
19
|
+
extra_deps << ['ruby_parser', '~> 2.0']
|
20
|
+
end
|
21
|
+
|
22
|
+
# vim: syntax=ruby
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.4.2
|
data/bin/flay
ADDED
data/lib/flay.rb
ADDED
@@ -0,0 +1,326 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
$: << "../../ruby_parser/dev/lib"
|
4
|
+
$: << "../../ruby2ruby/dev/lib"
|
5
|
+
|
6
|
+
require 'optparse'
|
7
|
+
require 'rubygems'
|
8
|
+
require 'sexp_processor'
|
9
|
+
require 'ruby_parser'
|
10
|
+
|
11
|
+
class Flay
|
12
|
+
VERSION = '1.4.2'
|
13
|
+
|
14
|
+
def self.default_options
|
15
|
+
{
|
16
|
+
:diff => false,
|
17
|
+
:mass => 16,
|
18
|
+
:summary => false,
|
19
|
+
:verbose => false,
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse_options
|
24
|
+
options = self.default_options
|
25
|
+
|
26
|
+
OptionParser.new do |opts|
|
27
|
+
opts.banner = 'flay [options] files_or_dirs'
|
28
|
+
opts.version = Flay::VERSION
|
29
|
+
|
30
|
+
opts.separator ""
|
31
|
+
opts.separator "Specific options:"
|
32
|
+
opts.separator ""
|
33
|
+
|
34
|
+
opts.on('-h', '--help', 'Display this help.') do
|
35
|
+
puts opts
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('-f', '--fuzzy', "DEAD: fuzzy similarities.") do
|
40
|
+
abort "--fuzzy is no longer supported. Sorry. It sucked."
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-m', '--mass MASS', Integer, "Sets mass threshold") do |m|
|
44
|
+
options[:mass] = m.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on('-v', '--verbose', "Verbose. Show progress processing files.") do
|
48
|
+
options[:verbose] = true
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on('-d', '--diff', "Diff Mode. Display N-Way diff for ruby.") do
|
52
|
+
options[:diff] = true
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on('-s', '--summary', "Summarize. Show flay score per file only.") do
|
56
|
+
options[:summary] = true
|
57
|
+
end
|
58
|
+
|
59
|
+
extensions = ['rb'] + Flay.load_plugins
|
60
|
+
|
61
|
+
opts.separator ""
|
62
|
+
opts.separator "Known extensions: #{extensions.join(', ')}"
|
63
|
+
|
64
|
+
begin
|
65
|
+
opts.parse!
|
66
|
+
rescue => e
|
67
|
+
abort "#{e}\n\n#{opts}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
options
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.expand_dirs_to_files *dirs
|
75
|
+
extensions = ['rb'] + Flay.load_plugins
|
76
|
+
|
77
|
+
dirs.flatten.map { |p|
|
78
|
+
if File.directory? p then
|
79
|
+
Dir[File.join(p, '**', "*.{#{extensions.join(',')}}")]
|
80
|
+
else
|
81
|
+
p
|
82
|
+
end
|
83
|
+
}.flatten
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.load_plugins
|
87
|
+
unless defined? @@plugins then
|
88
|
+
@@plugins = []
|
89
|
+
|
90
|
+
plugins = Gem.find_files("flay_*.rb").reject { |p| p =~ /flay_task/ }
|
91
|
+
|
92
|
+
plugins.each do |plugin|
|
93
|
+
plugin_name = File.basename(plugin, '.rb').sub(/^flay_/, '')
|
94
|
+
next if @@plugins.include? plugin_name
|
95
|
+
begin
|
96
|
+
load plugin
|
97
|
+
@@plugins << plugin_name
|
98
|
+
rescue LoadError => e
|
99
|
+
warn "error loading #{plugin.inspect}: #{e.message}. skipping..."
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
@@plugins
|
104
|
+
rescue
|
105
|
+
# ignore
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_accessor :mass_threshold, :total, :identical, :masses
|
109
|
+
attr_reader :hashes, :option
|
110
|
+
|
111
|
+
def initialize option = nil
|
112
|
+
@option = option || Flay.default_options
|
113
|
+
@hashes = Hash.new { |h,k| h[k] = [] }
|
114
|
+
|
115
|
+
self.identical = {}
|
116
|
+
self.masses = {}
|
117
|
+
self.total = 0
|
118
|
+
self.mass_threshold = @option[:mass]
|
119
|
+
|
120
|
+
require 'ruby2ruby' if @option[:diff]
|
121
|
+
end
|
122
|
+
|
123
|
+
def process(*files) # TODO: rename from process - should act as SexpProcessor
|
124
|
+
files.each do |file|
|
125
|
+
warn "Processing #{file}" if option[:verbose]
|
126
|
+
|
127
|
+
ext = File.extname(file).sub(/^\./, '')
|
128
|
+
ext = "rb" if ext.nil? || ext.empty?
|
129
|
+
msg = "process_#{ext}"
|
130
|
+
|
131
|
+
unless respond_to? msg then
|
132
|
+
warn " Unknown file type: #{ext}, defaulting to ruby"
|
133
|
+
msg = "process_rb"
|
134
|
+
end
|
135
|
+
|
136
|
+
begin
|
137
|
+
sexp = begin
|
138
|
+
send msg, file
|
139
|
+
rescue => e
|
140
|
+
warn " #{e.message.strip}"
|
141
|
+
warn " skipping #{file}"
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
|
145
|
+
next unless sexp
|
146
|
+
|
147
|
+
process_sexp sexp
|
148
|
+
rescue SyntaxError => e
|
149
|
+
warn " skipping #{file}: #{e.message}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
analyze
|
154
|
+
end
|
155
|
+
|
156
|
+
def analyze
|
157
|
+
self.prune
|
158
|
+
|
159
|
+
self.hashes.each do |hash,nodes|
|
160
|
+
identical[hash] = nodes[1..-1].all? { |n| n == nodes.first }
|
161
|
+
masses[hash] = nodes.first.mass * nodes.size
|
162
|
+
masses[hash] *= (nodes.size) if identical[hash]
|
163
|
+
self.total += masses[hash]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def process_rb file
|
168
|
+
RubyParser.new.process(File.read(file), file)
|
169
|
+
end
|
170
|
+
|
171
|
+
def process_sexp pt
|
172
|
+
pt.deep_each do |node|
|
173
|
+
next unless node.any? { |sub| Sexp === sub }
|
174
|
+
next if node.mass < self.mass_threshold
|
175
|
+
|
176
|
+
self.hashes[node.structural_hash] << node
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def prune
|
181
|
+
# prune trees that aren't duped at all, or are too small
|
182
|
+
self.hashes.delete_if { |_,nodes| nodes.size == 1 }
|
183
|
+
|
184
|
+
# extract all subtree hashes from all nodes
|
185
|
+
all_hashes = {}
|
186
|
+
self.hashes.values.each do |nodes|
|
187
|
+
nodes.each do |node|
|
188
|
+
node.all_structural_subhashes.each do |h|
|
189
|
+
all_hashes[h] = true
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# nuke subtrees so we show the biggest matching tree possible
|
195
|
+
self.hashes.delete_if { |h,_| all_hashes[h] }
|
196
|
+
end
|
197
|
+
|
198
|
+
def n_way_diff *data
|
199
|
+
data.each_with_index do |s, i|
|
200
|
+
c = (?A.ord + i).chr
|
201
|
+
s.group = c
|
202
|
+
end
|
203
|
+
|
204
|
+
max = data.map { |s| s.scan(/^.*/).size }.max
|
205
|
+
|
206
|
+
data.map! { |s| # FIX: this is tarded, but I'm out of brain
|
207
|
+
c = s.group
|
208
|
+
s = s.scan(/^.*/)
|
209
|
+
s.push(*([""] * (max - s.size))) # pad
|
210
|
+
s.each do |o|
|
211
|
+
o.group = c
|
212
|
+
end
|
213
|
+
s
|
214
|
+
}
|
215
|
+
|
216
|
+
groups = data[0].zip(*data[1..-1])
|
217
|
+
groups.map! { |lines|
|
218
|
+
collapsed = lines.uniq
|
219
|
+
if collapsed.size == 1 then
|
220
|
+
" #{lines.first}"
|
221
|
+
else
|
222
|
+
# TODO: make r2r have a canonical mode (doesn't make 1-liners)
|
223
|
+
lines.reject { |l| l.empty? }.map { |l| "#{l.group}: #{l}" }
|
224
|
+
end
|
225
|
+
}
|
226
|
+
groups.flatten.join("\n")
|
227
|
+
end
|
228
|
+
|
229
|
+
def summary
|
230
|
+
score = Hash.new 0
|
231
|
+
|
232
|
+
masses.each do |hash, mass|
|
233
|
+
sexps = hashes[hash]
|
234
|
+
mass_per_file = mass.to_f / sexps.size
|
235
|
+
sexps.each do |sexp|
|
236
|
+
score[sexp.file] += mass_per_file
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
score
|
241
|
+
end
|
242
|
+
|
243
|
+
def report prune = nil
|
244
|
+
puts "Total score (lower is better) = #{self.total}"
|
245
|
+
puts
|
246
|
+
|
247
|
+
if option[:summary] then
|
248
|
+
|
249
|
+
self.summary.sort_by { |_,v| -v }.each do |file, score|
|
250
|
+
puts "%8.2f: %s" % [score, file]
|
251
|
+
end
|
252
|
+
|
253
|
+
return
|
254
|
+
end
|
255
|
+
|
256
|
+
count = 0
|
257
|
+
masses.sort_by { |h,m| [-m, hashes[h].first.file] }.each do |hash, mass|
|
258
|
+
nodes = hashes[hash]
|
259
|
+
next unless nodes.first.first == prune if prune
|
260
|
+
puts
|
261
|
+
|
262
|
+
same = identical[hash]
|
263
|
+
node = nodes.first
|
264
|
+
n = nodes.size
|
265
|
+
match, bonus = if same then
|
266
|
+
["IDENTICAL", "*#{n}"]
|
267
|
+
else
|
268
|
+
["Similar", ""]
|
269
|
+
end
|
270
|
+
|
271
|
+
count += 1
|
272
|
+
puts "%d) %s code found in %p (mass%s = %d)" %
|
273
|
+
[count, match, node.first, bonus, mass]
|
274
|
+
|
275
|
+
nodes.each_with_index do |x, i|
|
276
|
+
if option[:diff] then
|
277
|
+
c = (?A.ord + i).chr
|
278
|
+
puts " #{c}: #{x.file}:#{x.line}"
|
279
|
+
else
|
280
|
+
puts " #{x.file}:#{x.line}"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
if option[:diff] then
|
285
|
+
puts
|
286
|
+
r2r = Ruby2Ruby.new
|
287
|
+
puts n_way_diff(*nodes.map { |s| r2r.process(s.deep_clone) })
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
class String
|
294
|
+
attr_accessor :group
|
295
|
+
end
|
296
|
+
|
297
|
+
class Sexp
|
298
|
+
def structural_hash
|
299
|
+
@structural_hash ||= self.structure.hash
|
300
|
+
end
|
301
|
+
|
302
|
+
def all_structural_subhashes
|
303
|
+
hashes = []
|
304
|
+
self.deep_each do |node|
|
305
|
+
hashes << node.structural_hash
|
306
|
+
end
|
307
|
+
hashes
|
308
|
+
end
|
309
|
+
|
310
|
+
# REFACTOR: move to sexp.rb
|
311
|
+
def deep_each(&block)
|
312
|
+
self.each_sexp do |sexp|
|
313
|
+
block[sexp]
|
314
|
+
sexp.deep_each(&block)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# REFACTOR: move to sexp.rb
|
319
|
+
def each_sexp
|
320
|
+
self.each do |sexp|
|
321
|
+
next unless Sexp === sexp
|
322
|
+
|
323
|
+
yield sexp
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
data/lib/flay_erb.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'flay'
|
5
|
+
require 'erb'
|
6
|
+
|
7
|
+
class Flay
|
8
|
+
def process_erb file
|
9
|
+
erb = File.read file
|
10
|
+
|
11
|
+
ruby = ERB.new(erb).src
|
12
|
+
begin
|
13
|
+
RubyParser.new.process(ruby, file)
|
14
|
+
rescue => e
|
15
|
+
warn ruby if option[:verbose]
|
16
|
+
raise e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/flay_task.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
class FlayTask < Rake::TaskLib
|
2
|
+
attr_accessor :name
|
3
|
+
attr_accessor :dirs
|
4
|
+
attr_accessor :threshold
|
5
|
+
attr_accessor :verbose
|
6
|
+
|
7
|
+
def initialize name = :flay, threshold = 200, dirs = nil
|
8
|
+
@name = name
|
9
|
+
@dirs = dirs || %w(app bin lib spec test)
|
10
|
+
@threshold = threshold
|
11
|
+
@verbose = Rake.application.options.trace
|
12
|
+
|
13
|
+
yield self if block_given?
|
14
|
+
|
15
|
+
@dirs.reject! { |f| ! File.directory? f }
|
16
|
+
|
17
|
+
define
|
18
|
+
end
|
19
|
+
|
20
|
+
def define
|
21
|
+
desc "Analyze for code duplication in: #{dirs.join(', ')}"
|
22
|
+
task name do
|
23
|
+
require "flay"
|
24
|
+
flay = Flay.new
|
25
|
+
flay.process(*Flay.expand_dirs_to_files(dirs))
|
26
|
+
flay.report if verbose
|
27
|
+
|
28
|
+
raise "Flay total too high! #{flay.total} > #{threshold}" if
|
29
|
+
flay.total > threshold
|
30
|
+
end
|
31
|
+
self
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/ruby -ws
|
2
|
+
|
3
|
+
$: << 'lib' << '../../ParseTree/dev/lib' << '../../flay/dev/lib'
|
4
|
+
|
5
|
+
$v ||= false # HACK
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'flay'
|
9
|
+
|
10
|
+
require 'gauntlet'
|
11
|
+
require 'pp'
|
12
|
+
|
13
|
+
class FlayGauntlet < Gauntlet
|
14
|
+
$owners = {}
|
15
|
+
$score_file = 'flay-scores.yml'
|
16
|
+
$misc_error = {:total => -1, :average => -1, :methods => {}}
|
17
|
+
$syntax_error = {:total => -2, :average => -2, :methods => {}}
|
18
|
+
$no_gem = {:total => -4, :average => -4, :methods => {}}
|
19
|
+
|
20
|
+
# copied straight from hoedown.rb
|
21
|
+
my_projects = %w[InlineFortran ParseTree RubyInline RubyToC
|
22
|
+
ZenHacks ZenTest bfts box_layout
|
23
|
+
change_class flay flog gauntlet heckle
|
24
|
+
hoe image_science miniunit minitest
|
25
|
+
minitest_tu_shim png ruby2ruby ruby_parser
|
26
|
+
rubyforge test-unit un vlad zenprofile
|
27
|
+
zentest]
|
28
|
+
|
29
|
+
MY_PROJECTS = Regexp.union(*my_projects)
|
30
|
+
|
31
|
+
def run name
|
32
|
+
warn name
|
33
|
+
self.data[name] = score_for '.'
|
34
|
+
self.dirty = true
|
35
|
+
end
|
36
|
+
|
37
|
+
def display_report max
|
38
|
+
good_data = {}
|
39
|
+
bad_count = 0
|
40
|
+
zero_count = 0
|
41
|
+
|
42
|
+
@data.each do |name, flay|
|
43
|
+
case
|
44
|
+
when flay < 0 then
|
45
|
+
bad_count += 1
|
46
|
+
when flay == 0 then
|
47
|
+
zero_count += 1
|
48
|
+
else
|
49
|
+
good_data[name] = flay
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
scores = good_data.values
|
54
|
+
|
55
|
+
# SWEET JESUS:
|
56
|
+
#
|
57
|
+
# without zeros:
|
58
|
+
# average flay: 1487.23 +/- 7800.16
|
59
|
+
# with zeros:
|
60
|
+
# average flay: 988.69 +/- 6398.45
|
61
|
+
|
62
|
+
puts "broken projects : %d" % bad_count
|
63
|
+
puts "great projects : %d" % zero_count
|
64
|
+
puts "bad projects : %d" % good_data.size
|
65
|
+
puts "average flay : %.2f +/- %.2f" % [scores.average, scores.stddev]
|
66
|
+
|
67
|
+
top = good_data.sort_by { |name,flay| -flay }.first max
|
68
|
+
|
69
|
+
puts
|
70
|
+
top.each_with_index do |(name, flay), i|
|
71
|
+
puts "%3d: %10.2f: %s" % [ i, flay, name ]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
############################################################
|
76
|
+
# OTHER
|
77
|
+
############################################################
|
78
|
+
|
79
|
+
def score_for dir
|
80
|
+
# files = `find #{dir} -name \\*.rb | grep -v gen.*templ`.split(/\n/)
|
81
|
+
flayer = Flay.new
|
82
|
+
|
83
|
+
dirs = %w(app lib test spec).reject { |f| ! File.directory? f }
|
84
|
+
|
85
|
+
flay = Flay.new
|
86
|
+
flay.process(*Flay.expand_dirs_to_files(dirs))
|
87
|
+
flay.total
|
88
|
+
rescue Interrupt
|
89
|
+
# let us break out
|
90
|
+
rescue Exception
|
91
|
+
-1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
max = (ARGV.shift || 10).to_i
|
96
|
+
filter = ARGV.shift
|
97
|
+
filter = Regexp.new filter if filter
|
98
|
+
flayer = FlayGauntlet.new
|
99
|
+
flayer.run_the_gauntlet filter
|
100
|
+
flayer.display_report max
|
data/test/test_flay.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'flay'
|
5
|
+
|
6
|
+
$: << "../../sexp_processor/dev/lib"
|
7
|
+
|
8
|
+
class TestSexp < MiniTest::Unit::TestCase
|
9
|
+
def setup
|
10
|
+
# a(1) { |c| d }
|
11
|
+
@s = s(:iter,
|
12
|
+
s(:call, nil, :a, s(:arglist, s(:lit, 1))),
|
13
|
+
s(:lasgn, :c),
|
14
|
+
s(:call, nil, :d, s(:arglist)))
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_structural_hash
|
18
|
+
hash = s(:iter,
|
19
|
+
s(:call, s(:arglist, s(:lit))),
|
20
|
+
s(:lasgn),
|
21
|
+
s(:call, s(:arglist))).hash
|
22
|
+
|
23
|
+
assert_equal hash, @s.structural_hash
|
24
|
+
assert_equal hash, @s.deep_clone.structural_hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_all_structural_subhashes
|
28
|
+
s = s(:iter,
|
29
|
+
s(:call, s(:arglist, s(:lit))),
|
30
|
+
s(:lasgn),
|
31
|
+
s(:call, s(:arglist)))
|
32
|
+
|
33
|
+
expected = [
|
34
|
+
s[1] .hash,
|
35
|
+
s[1][1] .hash,
|
36
|
+
s[1][1][1].hash,
|
37
|
+
s[2] .hash,
|
38
|
+
s[3] .hash,
|
39
|
+
s[3][1] .hash,
|
40
|
+
].sort
|
41
|
+
|
42
|
+
assert_equal expected, @s.all_structural_subhashes.sort.uniq
|
43
|
+
|
44
|
+
x = []
|
45
|
+
|
46
|
+
@s.deep_each do |o|
|
47
|
+
x << o.structural_hash
|
48
|
+
end
|
49
|
+
|
50
|
+
assert_equal expected, x.sort.uniq
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_process_sexp
|
54
|
+
flay = Flay.new
|
55
|
+
|
56
|
+
s = RubyParser.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
|
+
expected = [[:block],
|
67
|
+
# HACK [:defn],
|
68
|
+
[:scope]] # only ones big enough
|
69
|
+
|
70
|
+
flay.process_sexp s
|
71
|
+
|
72
|
+
actual = flay.hashes.values.map { |sexps| sexps.map { |sexp| sexp.first } }
|
73
|
+
|
74
|
+
assert_equal expected, actual.sort_by { |a| a.first.to_s }
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_process_sexp_full
|
78
|
+
flay = Flay.new(:mass => 1)
|
79
|
+
|
80
|
+
s = RubyParser.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
|
+
expected = [[:arglist, :arglist, :arglist],
|
91
|
+
[:block],
|
92
|
+
[:call, :call],
|
93
|
+
[:call],
|
94
|
+
[:if],
|
95
|
+
[:return],
|
96
|
+
[:return],
|
97
|
+
[:scope]]
|
98
|
+
|
99
|
+
flay.process_sexp s
|
100
|
+
|
101
|
+
actual = flay.hashes.values.map { |sexps| sexps.map { |sexp| sexp.first } }
|
102
|
+
|
103
|
+
assert_equal expected, actual.sort_by { |a| a.inspect }
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_process_sexp_no_structure
|
107
|
+
flay = Flay.new(:mass => 1)
|
108
|
+
flay.process_sexp s(:lit, 1)
|
109
|
+
|
110
|
+
assert flay.hashes.empty?
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_report
|
114
|
+
# make sure we run through options parser
|
115
|
+
$*.clear
|
116
|
+
$* << "-d"
|
117
|
+
$* << "--mass=1"
|
118
|
+
$* << "-v"
|
119
|
+
|
120
|
+
flay = Flay.new Flay.parse_options
|
121
|
+
|
122
|
+
s = RubyParser.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
|
136
|
+
flay.analyze
|
137
|
+
flay.report
|
138
|
+
end
|
139
|
+
end
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mmullis-flay
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 1.4.2
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Davis
|
9
|
+
- Seattle.rb
|
10
|
+
- Michael Mullis
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
|
15
|
+
date: 2011-03-04 00:00:00 -05:00
|
16
|
+
default_executable: flay
|
17
|
+
dependencies:
|
18
|
+
- !ruby/object:Gem::Dependency
|
19
|
+
name: sexp_processor
|
20
|
+
prerelease: false
|
21
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
22
|
+
none: false
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: "3.0"
|
27
|
+
type: :runtime
|
28
|
+
version_requirements: *id001
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: ruby_parser
|
31
|
+
prerelease: false
|
32
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: "2.0"
|
38
|
+
type: :runtime
|
39
|
+
version_requirements: *id002
|
40
|
+
description: Flay fork with modifications to run on 1.9.2
|
41
|
+
email: michael@mullistechnologies.com
|
42
|
+
executables:
|
43
|
+
- flay
|
44
|
+
extensions: []
|
45
|
+
|
46
|
+
extra_rdoc_files:
|
47
|
+
- README.txt
|
48
|
+
files:
|
49
|
+
- History.txt
|
50
|
+
- Manifest.txt
|
51
|
+
- README.txt
|
52
|
+
- Rakefile
|
53
|
+
- VERSION
|
54
|
+
- bin/flay
|
55
|
+
- lib/flay.rb
|
56
|
+
- lib/flay_erb.rb
|
57
|
+
- lib/flay_task.rb
|
58
|
+
- lib/gauntlet_flay.rb
|
59
|
+
- test/test_flay.rb
|
60
|
+
has_rdoc: true
|
61
|
+
homepage: http://github.com/mmullis/flay
|
62
|
+
licenses: []
|
63
|
+
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.5.3
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: Flay fork with 1.9.2 Repairs
|
88
|
+
test_files:
|
89
|
+
- test/test_flay.rb
|