doppelganger 0.8.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.
@@ -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
+