mmullis-flay 1.4.2
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/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
|