doppelganger 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ == 0.8.0 / 2008-11-18
2
+
3
+ * Colorize CLI output
4
+ * Add progress bar in CLI
5
+ * Improve performance where possible
6
+ * Change to using Ruby2Ruby in CLI output when verbose
7
+ * Use custom UnifiedRuby instead of the one in ParseTree
8
+ * Separate analysis into two independent classes
9
+ * +Extractor+ extracts block-like nodes from code
10
+ * +NodeAnalysis+ analyzes for duplicates/similar nodes
11
+
12
+ == 0.7.0 / 2008-11-09
13
+
14
+ * Exclude literals from comparison (from Flay)
15
+ * Compare blocks as well as methods
16
+ * Move RubyParser from ParseTree
17
+ * Cleanup formating of CLI output
18
+ * Remove Unique node reporting
19
+ * Remove unneeded view/Ruby2Ruby functionality
20
+ * Improve and Simplify tests
21
+ * Change to using Test::Unit
22
+ * Improve Documentation
23
+
24
+ == 0.2.0 / 2008-11-06
25
+
26
+ * Add percent different functionality
27
+ * Name change to Doppelganger
28
+
29
+ == 0.1.0 / 2008-10-01
30
+
31
+ * Initial conversion to a gem
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2008 Brian Landau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,32 @@
1
+ CHANGELOG
2
+ LICENSE
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/doppelganger
7
+ doppelganger.gemspec
8
+ lib/doppelganger.rb
9
+ lib/doppelganger/extractor.rb
10
+ lib/doppelganger/exts/array.rb
11
+ lib/doppelganger/exts/sexp.rb
12
+ lib/doppelganger/node_analysis.rb
13
+ lib/doppelganger/unified_ruby.rb
14
+ tasks/bones.rake
15
+ tasks/gem.rake
16
+ tasks/git.rake
17
+ tasks/manifest.rake
18
+ tasks/post_load.rake
19
+ tasks/rdoc.rake
20
+ tasks/rubyforge.rake
21
+ tasks/setup.rb
22
+ tasks/test.rake
23
+ test/array_test.rb
24
+ test/doppelganger_test.rb
25
+ test/sample_files/duplicate_test_data/first_file.rb
26
+ test/sample_files/duplicate_test_data/second_file.rb
27
+ test/sample_files/larger_diff/first_file.rb
28
+ test/sample_files/larger_diff/second_file.rb
29
+ test/sample_files/repeats_removal_sample_file.rb
30
+ test/sample_files/sexp_test_file.rb
31
+ test/sexp_ext_test.rb
32
+ test/test_helper.rb
@@ -0,0 +1,67 @@
1
+ = Doppelganger
2
+
3
+ by Brian Landau of Viget Labs <brian.landau@viget.com>
4
+
5
+ [<b>GitHub Project</b>] http://github.com/brianjlandau/doppelganger
6
+ [<b>RubyForge Project</b>] http://rubyforge.org/projects/doppelganger/
7
+ [<b>RDoc</b>] http://doppelganger.rubyforge.org/
8
+
9
+
10
+ == DESCRIPTION:
11
+
12
+ Doppelganger helps you to find areas of your code that are good places to refactor.
13
+
14
+ It does this by finding methods and blocks that are duplicates or have less then
15
+ a set threshold level of difference or be less then a specified percent different.
16
+ This library can either be used with in another larger code metric/heuristic library, or
17
+ called from the command line.
18
+
19
+
20
+ == FEATURES:
21
+
22
+ * Find duplicate methods and blocks.
23
+ * Find methods and blocks similar by a threshold level of difference.
24
+ * Find methods and blocks similar by threshold percentage.
25
+ * Uses the Diff::LCS library for finding differences.
26
+ * Uses RubyParser to compare methods.
27
+ * Outputs the set of similar/duplicate nodes together and what files they are in and their line numbers.
28
+
29
+ == CAVEATS:
30
+
31
+ Doing the Diff::LCS on all the flattened Sexps is a very time consuming and processor
32
+ intense operation. This means doing the "diff" or "percent_diff" comparisons on large
33
+ libraries can take a very long time.
34
+
35
+
36
+ == REQUIREMENTS:
37
+
38
+ * ruby_parser
39
+ * sexp_processor
40
+ * Diff::LCS
41
+ * Ruby2Ruby
42
+ * HighLine
43
+ * Facets
44
+
45
+
46
+ == USAGE:
47
+
48
+ Find duplicates and methods have 5 or less differences.
49
+
50
+ $ doppelganger -n 5 project_dir/lib
51
+
52
+ Find duplicates and methods that are 10 percent different or less.
53
+
54
+ $ doppelganger -p 10 project_dir/lib
55
+
56
+
57
+ == INSTALL:
58
+
59
+ sudo gem install doppelganger
60
+
61
+
62
+ === CREDITS:
63
+
64
+ Inspired and based off of Giles Bowkett's Towelie.
65
+
66
+ Parts also influenced or extracted by Ryan Davis' Flay.
67
+
@@ -0,0 +1,32 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ load 'tasks/setup.rb'
6
+
7
+ ensure_in_path 'lib'
8
+ require 'doppelganger'
9
+
10
+ task :default => 'test:run'
11
+
12
+ PROJ.name = 'doppelganger'
13
+ PROJ.authors = 'Brian Landau'
14
+ PROJ.email = 'brian.landau@viget.com'
15
+ PROJ.url = 'http://doppelganger.rubyforge.org/'
16
+ PROJ.version = Doppelganger::VERSION
17
+ PROJ.rubyforge.name = 'doppelganger'
18
+
19
+ PROJ.rcov.opts = ['--no-html', '-T', '--sort coverage',
20
+ '-x "\/Library\/Ruby\/"',
21
+ '-x "\/opt\/local\/lib/ruby"',
22
+ '-x "\/System\/Library\/"']
23
+
24
+
25
+ PROJ.gem.development_dependencies = [['thoughtbot-shoulda', '~> 2.0']]
26
+ depend_on 'sexp_processor', '~> 3.0.0'
27
+ depend_on 'ruby_parser', '~> 2.0.0'
28
+ depend_on 'ruby2ruby', '~> 1.2.0'
29
+ depend_on 'diff-lcs', '~> 1.1'
30
+ depend_on 'highline', '~> 1.4'
31
+ depend_on 'facets', '~> 2.4'
32
+
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'ostruct'
5
+ require 'facets/progressbar'
6
+ require File.expand_path( File.join(File.dirname(__FILE__), %w[.. lib doppelganger]) )
7
+
8
+
9
+ class Doppelganger::CLI
10
+ def self.parse(args)
11
+ options = OpenStruct.new
12
+ options.verbose = false
13
+ options.color = true
14
+ options.progress_bar = true
15
+
16
+ opts = OptionParser.new do |opts|
17
+ opts.program_name = 'doppelganger'
18
+ opts.version = Doppelganger.version
19
+ opts.banner = "Usage: #{opts.program_name} [options] DIRECTORY"
20
+
21
+ opts.separator " "
22
+ opts.separator "Options:"
23
+
24
+ opts.on('-t', "--threshold [N]", Integer, "The number of node differences to print blocks for.") do |n|
25
+ options.threshold = n
26
+ end
27
+
28
+ opts.on('-p', "--percentage [N]", Integer, "The percent difference to look for in two blocks.") do |n|
29
+ options.percentage = n
30
+ end
31
+
32
+ opts.on('-v', "--[no-]verbose", "Display compared nodes when matched.") do |v|
33
+ options.verbose = v
34
+ end
35
+
36
+ opts.on('--[no-]color', "Don't use color output.") do |c|
37
+ options.color = c
38
+ end
39
+
40
+ opts.on('--[no-]progress-bar', "Don't show the progress bar on Diffs.") do |pb|
41
+ options.progress_bar = pb
42
+ end
43
+
44
+ opts.separator " "
45
+
46
+ opts.on_tail("-h", "--help", "Show this message") do
47
+ puts opts.help
48
+ exit
49
+ end
50
+
51
+ opts.on_tail("--version", "Show version") do
52
+ puts opts.ver
53
+ exit
54
+ end
55
+ end
56
+
57
+ opts.parse!(args)
58
+ if ARGV.length < 1
59
+ puts "ERROR: Missing expected arguments. ou must specify a directory parameter."
60
+ puts
61
+ puts opts.help
62
+ exit
63
+ elsif ARGV.length > 1
64
+ puts "ERROR: Too many arguments. Only a directory is needed."
65
+ puts
66
+ puts opts.help
67
+ exit
68
+ else
69
+ options.dir = ARGV.shift
70
+ end
71
+
72
+ @options = options
73
+ @root_path = File.expand_path(@options.dir)
74
+ end
75
+
76
+ def self.run(args)
77
+ parse(args)
78
+
79
+ if @options.color
80
+ gem 'highline', '~> 1.4'
81
+ require 'highline'
82
+ HighLine.color_scheme = HighLine::ColorScheme.new do |cs|
83
+ cs[:headline] = [:bold, :magenta]
84
+ cs[:set_start] = [:cyan]
85
+ cs[:horizontal_line] = [:white]
86
+ cs[:block_item] = [:red]
87
+ cs[:code] = [:green]
88
+ end
89
+ @hl = HighLine.new
90
+ end
91
+
92
+ @doppelganger = Doppelganger::Extractor.new
93
+ @sexp_blocks = @doppelganger.extract_blocks(@options.dir)
94
+ @analysis = Doppelganger::NodeAnalysis.new(@sexp_blocks)
95
+ @max_filename_length = longest_filename(@sexp_blocks)
96
+
97
+ if @options.verbose
98
+ gem 'ruby2ruby', '~> 1.2.0'
99
+ require 'ruby2ruby'
100
+ @ruby2ruby = Ruby2Ruby.new
101
+ end
102
+
103
+ duplicates = @analysis.duplicates
104
+ display_block_sets duplicates, "=== DUPLICATES: ===\n", ' -- [DUPLICATED BLOCK SET]:'
105
+
106
+ if @options.threshold
107
+ compare_nodes(:diff, @options.threshold, "MAX DIFF of #{@options.threshold} NODES")
108
+ end
109
+
110
+ if @options.percentage
111
+ compare_nodes(:percent_diff, @options.percentage, "PERCENT DIFF of #{@options.percentage}%")
112
+ end
113
+ end
114
+
115
+ def self.compare_nodes(method, diff_size, title)
116
+ print_text('-' * 80, :horizontal_line)
117
+ puts
118
+ if @options.progress_bar
119
+ progress_bar = ProgressBar.new("DIFF Progress", @sexp_blocks.size**2)
120
+ progress_bar.bar_mark = '='
121
+ similar_methods = @analysis.send(method, diff_size, progress_bar)
122
+ progress_bar.finish
123
+ puts
124
+ else
125
+ similar_methods = @analysis.send(method, diff_size)
126
+ end
127
+ display_block_sets similar_methods, "=== #{title}: ===\n", ' -- [SIMILAR BLOCK PAIR]:'
128
+ end
129
+
130
+ def self.display_block_sets(set, title, set_title)
131
+ unless set.empty?
132
+ print_text(title, :headline)
133
+ puts unless @options.color
134
+ set.each do |block_set|
135
+ print_text(set_title, :set_start)
136
+ block_set.each do |block_node|
137
+ if block_node.respond_to?(:name)
138
+ display_filename_line_and_method_name(block_node)
139
+ else
140
+ display_filename_and_line(block_node)
141
+ end
142
+ if @options.verbose
143
+ print_text(@ruby2ruby.process(block_node.node.deep_clone).gsub(/\n/, "\n"+(' '*6)).insert(0, ' '*6), :code)
144
+ puts
145
+ end
146
+ end
147
+ end
148
+ puts
149
+ end
150
+ end
151
+
152
+ def self.display_filename_line_and_method_name(method_defn)
153
+ print_text(sprintf(' -> %1$*4$s:#%2$*5$d => `%3$s`', format_filename(method_defn.filename),
154
+ method_defn.line.to_i, method_defn.name,
155
+ @max_filename_length.to_i, -5), :block_item)
156
+ end
157
+
158
+ def self.display_filename_and_line(block_node)
159
+ print_text(sprintf(' -> %1$*3$s:#%2$*4$d', format_filename(block_node.filename),
160
+ block_node.line.to_i, @max_filename_length.to_i, -5), :block_item)
161
+ end
162
+
163
+ def self.format_filename(filename)
164
+ relative_filename = filename.gsub(@root_path, '.')
165
+ if relative_filename.size > @max_filename_length
166
+ output_filename = relative_filename[-@max_filename_length, @max_filename_length]
167
+ output_filename[0..2] = '...'
168
+ else
169
+ output_filename = relative_filename
170
+ end
171
+ output_filename
172
+ end
173
+
174
+ def self.longest_filename(nodes)
175
+ max_filename_length = nodes.map{|n| n.filename.gsub(@root_path, '.').size}.max
176
+ max_filename_length > 50 ? 50 : max_filename_length
177
+ end
178
+
179
+ def self.print_text(text, color)
180
+ if @options.color
181
+ @hl.say(@hl.color(text, color))
182
+ else
183
+ puts text
184
+ end
185
+ end
186
+ end
187
+
188
+ Doppelganger::CLI.run(ARGV)
189
+
@@ -0,0 +1,53 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{doppelganger}
3
+ s.version = "0.8.0"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Brian Landau"]
7
+ s.date = %q{2008-11-18}
8
+ s.default_executable = %q{doppelganger}
9
+ s.description = %q{Doppelganger helps you to find areas of your code that are good places to refactor. It does this by finding methods and blocks that are duplicates or have less then a set threshold level of difference or be less then a specified percent different. This library can either be used with in another larger code metric/heuristic library, or called from the command line.}
10
+ s.email = %q{brian.landau@viget.com}
11
+ s.executables = ["doppelganger"]
12
+ s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "README.rdoc"]
13
+ s.files = ["CHANGELOG", "LICENSE", "Manifest.txt", "README.rdoc", "Rakefile", "bin/doppelganger", "doppelganger.gemspec", "lib/doppelganger.rb", "lib/doppelganger/extractor.rb", "lib/doppelganger/exts/array.rb", "lib/doppelganger/exts/sexp.rb", "lib/doppelganger/node_analysis.rb", "lib/doppelganger/unified_ruby.rb", "tasks/bones.rake", "tasks/gem.rake", "tasks/git.rake", "tasks/manifest.rake", "tasks/post_load.rake", "tasks/rdoc.rake", "tasks/rubyforge.rake", "tasks/setup.rb", "tasks/test.rake", "test/array_test.rb", "test/doppelganger_test.rb", "test/sample_files/duplicate_test_data/first_file.rb", "test/sample_files/duplicate_test_data/second_file.rb", "test/sample_files/larger_diff/first_file.rb", "test/sample_files/larger_diff/second_file.rb", "test/sample_files/repeats_removal_sample_file.rb", "test/sample_files/sexp_test_file.rb", "test/sexp_ext_test.rb", "test/test_helper.rb"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://doppelganger.rubyforge.org/}
16
+ s.rdoc_options = ["--main", "README.rdoc"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{doppelganger}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{Doppelganger helps you to find areas of your code that are good places to refactor}
21
+ s.test_files = ["test/array_test.rb", "test/doppelganger_test.rb", "test/sexp_ext_test.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 2
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<sexp_processor>, ["~> 3.0.0"])
29
+ s.add_runtime_dependency(%q<ruby_parser>, ["~> 2.0.0"])
30
+ s.add_runtime_dependency(%q<ruby2ruby>, ["~> 1.2.0"])
31
+ s.add_runtime_dependency(%q<diff-lcs>, ["~> 1.1"])
32
+ s.add_runtime_dependency(%q<highline>, ["~> 1.4"])
33
+ s.add_runtime_dependency(%q<facets>, ["~> 2.4"])
34
+ s.add_development_dependency(%q<thoughtbot-shoulda>, ["~> 2.0"])
35
+ else
36
+ s.add_dependency(%q<sexp_processor>, ["~> 3.0.0"])
37
+ s.add_dependency(%q<ruby_parser>, ["~> 2.0.0"])
38
+ s.add_dependency(%q<ruby2ruby>, ["~> 1.2.0"])
39
+ s.add_dependency(%q<diff-lcs>, ["~> 1.1"])
40
+ s.add_dependency(%q<highline>, ["~> 1.4"])
41
+ s.add_dependency(%q<facets>, ["~> 2.4"])
42
+ s.add_dependency(%q<thoughtbot-shoulda>, ["~> 2.0"])
43
+ end
44
+ else
45
+ s.add_dependency(%q<sexp_processor>, ["~> 3.0.0"])
46
+ s.add_dependency(%q<ruby_parser>, ["~> 2.0.0"])
47
+ s.add_dependency(%q<ruby2ruby>, ["~> 1.2.0"])
48
+ s.add_dependency(%q<diff-lcs>, ["~> 1.1"])
49
+ s.add_dependency(%q<highline>, ["~> 1.4"])
50
+ s.add_dependency(%q<facets>, ["~> 2.4"])
51
+ s.add_dependency(%q<thoughtbot-shoulda>, ["~> 2.0"])
52
+ end
53
+ end
@@ -0,0 +1,63 @@
1
+ require 'find'
2
+ require 'rubygems'
3
+ gem 'sexp_processor', '~> 3.0.0'
4
+ require 'sexp_processor'
5
+ require 'composite_sexp_processor'
6
+ gem 'ruby_parser', '~> 2.0.0'
7
+ require 'ruby_parser'
8
+ gem 'diff-lcs', '~> 1.1'
9
+ require 'diff/lcs'
10
+
11
+ # Equivalent to a header guard in C/C++
12
+ # Used to prevent the class/module from being loaded more than once
13
+ unless defined? Doppelganger
14
+
15
+ module Doppelganger
16
+
17
+ # :stopdoc:
18
+ VERSION = '0.8.0'.freeze
19
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
20
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
21
+
22
+ # Returns the version string for the library.
23
+ #
24
+ def self.version
25
+ VERSION
26
+ end
27
+
28
+ # Returns the library path for the module. If any arguments are given,
29
+ # they will be joined to the end of the libray path using
30
+ # <tt>File.join</tt>.
31
+ #
32
+ def self.libpath( *args )
33
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
34
+ end
35
+
36
+ # Returns the lpath for the module. If any arguments are given,
37
+ # they will be joined to the end of the path using
38
+ # <tt>File.join</tt>.
39
+ #
40
+ def self.path( *args )
41
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
42
+ end
43
+
44
+ # Utility method used to rquire all files ending in .rb that lie in the
45
+ # directory below this file that has the same name as the filename passed
46
+ # in. Optionally, a specific _directory_ name can be passed in such that
47
+ # the _filename_ does not have to be equivalent to the directory.
48
+ #
49
+ def self.require_all_libs_relative_to( fname, dir = nil )
50
+ dir ||= ::File.basename(fname, '.*')
51
+ search_me = ::File.expand_path(
52
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
53
+
54
+ Dir.glob(search_me).sort.each {|rb| require rb}
55
+ end
56
+ # :startdoc:
57
+
58
+ end # module Doppelganger
59
+
60
+ Doppelganger.require_all_libs_relative_to __FILE__
61
+
62
+ end # unless defined?
63
+