teksymmetry-reek 1.1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/History.txt +131 -0
  2. data/README.txt +36 -0
  3. data/Rakefile +17 -0
  4. data/bin/reek +27 -0
  5. data/config/defaults.reek +51 -0
  6. data/lib/reek/block_context.rb +59 -0
  7. data/lib/reek/class_context.rb +68 -0
  8. data/lib/reek/code_context.rb +54 -0
  9. data/lib/reek/code_parser.rb +221 -0
  10. data/lib/reek/exceptions.reek +13 -0
  11. data/lib/reek/if_context.rb +25 -0
  12. data/lib/reek/method_context.rb +91 -0
  13. data/lib/reek/module_context.rb +33 -0
  14. data/lib/reek/name.rb +49 -0
  15. data/lib/reek/object_refs.rb +52 -0
  16. data/lib/reek/object_source.rb +53 -0
  17. data/lib/reek/options.rb +100 -0
  18. data/lib/reek/rake_task.rb +121 -0
  19. data/lib/reek/report.rb +81 -0
  20. data/lib/reek/sexp_formatter.rb +10 -0
  21. data/lib/reek/singleton_method_context.rb +27 -0
  22. data/lib/reek/smell_warning.rb +49 -0
  23. data/lib/reek/smells/control_couple.rb +61 -0
  24. data/lib/reek/smells/duplication.rb +50 -0
  25. data/lib/reek/smells/feature_envy.rb +58 -0
  26. data/lib/reek/smells/large_class.rb +69 -0
  27. data/lib/reek/smells/long_method.rb +43 -0
  28. data/lib/reek/smells/long_parameter_list.rb +43 -0
  29. data/lib/reek/smells/long_yield_list.rb +18 -0
  30. data/lib/reek/smells/nested_iterators.rb +28 -0
  31. data/lib/reek/smells/smell_detector.rb +66 -0
  32. data/lib/reek/smells/smells.rb +81 -0
  33. data/lib/reek/smells/uncommunicative_name.rb +97 -0
  34. data/lib/reek/smells/utility_function.rb +34 -0
  35. data/lib/reek/source.rb +127 -0
  36. data/lib/reek/spec.rb +146 -0
  37. data/lib/reek/stop_context.rb +50 -0
  38. data/lib/reek/yield_call_context.rb +12 -0
  39. data/lib/reek.rb +7 -0
  40. data/reek.gemspec +44 -0
  41. data/spec/reek/block_context_spec.rb +40 -0
  42. data/spec/reek/class_context_spec.rb +169 -0
  43. data/spec/reek/code_context_spec.rb +93 -0
  44. data/spec/reek/code_parser_spec.rb +34 -0
  45. data/spec/reek/config_spec.rb +42 -0
  46. data/spec/reek/if_context_spec.rb +17 -0
  47. data/spec/reek/method_context_spec.rb +66 -0
  48. data/spec/reek/module_context_spec.rb +38 -0
  49. data/spec/reek/name_spec.rb +13 -0
  50. data/spec/reek/object_refs_spec.rb +131 -0
  51. data/spec/reek/options_spec.rb +13 -0
  52. data/spec/reek/report_spec.rb +48 -0
  53. data/spec/reek/singleton_method_context_spec.rb +17 -0
  54. data/spec/reek/smells/control_couple_spec.rb +23 -0
  55. data/spec/reek/smells/duplication_spec.rb +81 -0
  56. data/spec/reek/smells/feature_envy_spec.rb +221 -0
  57. data/spec/reek/smells/large_class_spec.rb +87 -0
  58. data/spec/reek/smells/long_method_spec.rb +195 -0
  59. data/spec/reek/smells/long_parameter_list_spec.rb +85 -0
  60. data/spec/reek/smells/nested_iterators_spec.rb +33 -0
  61. data/spec/reek/smells/smell_spec.rb +24 -0
  62. data/spec/reek/smells/uncommunicative_name_spec.rb +123 -0
  63. data/spec/reek/smells/utility_function_spec.rb +93 -0
  64. data/spec/slow/inline_spec.rb +40 -0
  65. data/spec/slow/optparse_spec.rb +109 -0
  66. data/spec/slow/redcloth_spec.rb +101 -0
  67. data/spec/slow/reek_source_spec.rb +20 -0
  68. data/spec/slow/samples/inline.rb +704 -0
  69. data/spec/slow/samples/optparse.rb +1788 -0
  70. data/spec/slow/samples/redcloth.rb +1130 -0
  71. data/spec/slow/script_spec.rb +55 -0
  72. data/spec/slow/source_list_spec.rb +40 -0
  73. data/spec/spec.opts +1 -0
  74. data/spec/spec_helper.rb +13 -0
  75. data/tasks/reek.rake +7 -0
  76. data/tasks/rspec.rake +22 -0
  77. metadata +163 -0
@@ -0,0 +1,91 @@
1
+ require 'reek/name'
2
+ require 'reek/code_context'
3
+ require 'reek/object_refs'
4
+
5
+ module Reek
6
+ class MethodContext < CodeContext
7
+ attr_reader :parameters
8
+ attr_reader :calls
9
+ attr_reader :refs
10
+ attr_reader :num_statements
11
+
12
+ def initialize(outer, exp, record = true)
13
+ super(outer, exp)
14
+ @parameters = []
15
+ @local_variables = []
16
+ @name = Name.new(exp[1])
17
+ @num_statements = 0
18
+ @calls = Hash.new(0)
19
+ @depends_on_self = false
20
+ @refs = ObjectRefs.new
21
+ @outer.record_method(@name) # TODO: should be children of outer?
22
+ end
23
+
24
+ def count_statements(num)
25
+ @num_statements += num
26
+ end
27
+
28
+ def depends_on_instance?
29
+ @depends_on_self || is_overriding_method?(@name)
30
+ end
31
+
32
+ def has_parameter(sym)
33
+ @parameters.include?(sym.to_s)
34
+ end
35
+
36
+ def record_call_to(exp)
37
+ @calls[exp] += 1
38
+ receiver, meth = exp[1..2]
39
+ receiver ||= [:self]
40
+ case receiver[0]
41
+ when :lvar
42
+ @refs.record_ref(receiver) unless meth == :new
43
+ when :self
44
+ record_use_of_self
45
+ end
46
+ end
47
+
48
+ def record_use_of_self
49
+ record_depends_on_self
50
+ @refs.record_reference_to_self
51
+ end
52
+
53
+ def record_instance_variable(sym)
54
+ record_use_of_self
55
+ @outer.record_instance_variable(sym)
56
+ end
57
+
58
+ def record_depends_on_self
59
+ @depends_on_self = true
60
+ end
61
+
62
+ def record_local_variable(sym)
63
+ @local_variables << Name.new(sym)
64
+ end
65
+
66
+ def self.is_block_arg?(param)
67
+ (Array === param and param[0] == :block) or (param.to_s =~ /^\&/)
68
+ end
69
+
70
+ def record_parameter(param)
71
+ @parameters << Name.new(param) unless MethodContext.is_block_arg?(param)
72
+ end
73
+
74
+ def outer_name
75
+ "#{@outer.outer_name}#{@name}/"
76
+ end
77
+
78
+ def to_s
79
+ "#{@outer.outer_name}#{@name}"
80
+ end
81
+
82
+ def envious_receivers
83
+ return [] if @refs.self_is_max?
84
+ @refs.max_keys
85
+ end
86
+
87
+ def variable_names
88
+ @parameters + @local_variables
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,33 @@
1
+ require 'reek/code_context'
2
+
3
+ module Reek
4
+ class ModuleContext < CodeContext
5
+
6
+ def ModuleContext.create(outer, exp)
7
+ res = Name.resolve(exp[1], outer)
8
+ ModuleContext.new(res[0], res[1])
9
+ end
10
+
11
+ def initialize(outer, name)
12
+ super(outer, nil)
13
+ @name = name
14
+ end
15
+
16
+ def myself
17
+ @myself ||= @outer.find_module(@name)
18
+ end
19
+
20
+ def find_module(modname)
21
+ return nil unless myself
22
+ @myself.const_or_nil(modname.to_s)
23
+ end
24
+
25
+ def outer_name
26
+ "#{@outer.outer_name}#{@name}::"
27
+ end
28
+
29
+ def variable_names
30
+ []
31
+ end
32
+ end
33
+ end
data/lib/reek/name.rb ADDED
@@ -0,0 +1,49 @@
1
+ module Reek
2
+ class Name
3
+ include Comparable
4
+
5
+ def self.resolve(exp, context)
6
+ unless Array === exp
7
+ return resolve_string(exp.to_s, context)
8
+ end
9
+ name = exp[1]
10
+ case exp[0]
11
+ when :colon2
12
+ return [resolve(name, context)[0], new(exp[2])]
13
+ when :const
14
+ return [ModuleContext.create(context, exp), new(name)]
15
+ when :colon3
16
+ return [StopContext.new, new(name)]
17
+ else
18
+ return [context, new(name)]
19
+ end
20
+ end
21
+
22
+ def self.resolve_string(str, context)
23
+ return [context, new(str)] unless str =~ /::/
24
+ resolve(RubyParser.new.parse(str), context)
25
+ end
26
+
27
+ def initialize(sym)
28
+ @name = sym.to_s
29
+ end
30
+
31
+ def hash # :nodoc:
32
+ @name.hash
33
+ end
34
+
35
+ def <=>(other) # :nodoc:
36
+ @name <=> other.to_s
37
+ end
38
+
39
+ alias eql? <=>
40
+
41
+ def effective_name
42
+ @name.gsub(/^@*/, '')
43
+ end
44
+
45
+ def to_s
46
+ @name
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'sexp_processor'
3
+
4
+ module Reek
5
+
6
+ class ObjectRefs # :nodoc:
7
+ def initialize
8
+ @refs = Hash.new(0)
9
+ end
10
+
11
+ def record_reference_to_self
12
+ record_ref(SELF_REF)
13
+ end
14
+
15
+ def record_ref(exp)
16
+ type = exp[0]
17
+ case type
18
+ when :gvar
19
+ return
20
+ when :self
21
+ record_reference_to_self
22
+ else
23
+ @refs[exp] += 1
24
+ end
25
+ end
26
+
27
+ def refs_to_self
28
+ @refs[SELF_REF]
29
+ end
30
+
31
+ def max_refs
32
+ @refs.values.max or 0
33
+ end
34
+
35
+ # TODO
36
+ # Should be moved to Hash
37
+ #
38
+ def max_keys
39
+ max = max_refs
40
+ @refs.reject {|key,val| val != max}.keys
41
+ end
42
+
43
+ def self_is_max?
44
+ max_keys.length == 0 || @refs[SELF_REF] == max_refs
45
+ end
46
+
47
+ private
48
+
49
+ SELF_REF = Sexp.from_array([:lit, :self])
50
+
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ module Reek
2
+ class Source
3
+ #
4
+ # Factory method: creates a +Source+ from obj.
5
+ # The code is not parsed until +report+ is called.
6
+ # (This feature is only enabled if you have the ParseTree gem installed.)
7
+ #
8
+ def self.from_object(obj)
9
+ return ObjectSource.new(obj, obj.to_s)
10
+ end
11
+ end
12
+
13
+ class ObjectSource < Source # :nodoc:
14
+
15
+ def self.unify(sexp) # :nodoc:
16
+ unifier = Unifier.new
17
+ unifier.processors.each do |proc|
18
+ proc.unsupported.delete :cfunc # HACK
19
+ end
20
+ return unifier.process(sexp[0])
21
+ end
22
+
23
+ def can_parse_objects?
24
+ return true if Object.const_defined?(:ParseTree)
25
+ begin
26
+ require 'parse_tree'
27
+ true
28
+ rescue LoadError
29
+ false
30
+ end
31
+ end
32
+
33
+ def generate_syntax_tree
34
+ if can_parse_objects?
35
+ ObjectSource.unify(ParseTree.new.parse_tree(@source))
36
+ else
37
+ throw ArgumentError.new('You must install the ParseTree gem to use this feature')
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class Object
44
+ #
45
+ # Constructs a Source representing this object; the source can then be used
46
+ # to generate an abstract syntax tree for the object, which can in turn then
47
+ # be examined for code smells.
48
+ # (This feature is only enabled if you have the ParseTree gem installed.)
49
+ #
50
+ def to_source
51
+ Reek::Source.from_object(self)
52
+ end
53
+ end
@@ -0,0 +1,100 @@
1
+ require 'optparse'
2
+ require 'reek/source'
3
+
4
+ module Reek
5
+
6
+ class Options
7
+
8
+ CTX_SORT = '%c %w (%s)' if !const_defined?(:CTX_SORT)
9
+ SMELL_SORT = '[%s] %c %w' if !const_defined?(:SMELL_SORT)
10
+
11
+ def self.default_options
12
+ {
13
+ :format => CTX_SORT
14
+ }
15
+ end
16
+
17
+ @@opts = default_options
18
+
19
+ def self.[](key)
20
+ @@opts[key]
21
+ end
22
+
23
+ def self.parse_args(args)
24
+ result = default_options
25
+ parser = OptionParser.new { |opts| set_options(opts, result) }
26
+ parser.parse!(args)
27
+ result
28
+ end
29
+
30
+ def self.set_options(opts, config)
31
+ opts.banner = <<EOB
32
+ Usage: #{opts.program_name} [options] files...
33
+
34
+ If no files are given, Reek reads source code from standard input.
35
+ See http://wiki.github.com/kevinrutherford/reek for detailed help.
36
+ EOB
37
+
38
+ opts.separator "\nOptions:"
39
+ set_help_option(opts)
40
+ set_sort_option(config, opts)
41
+ set_version_option(opts)
42
+ set_output_format_option(config, opts)
43
+ end
44
+
45
+ def self.parse(args)
46
+ begin
47
+ @@opts = parse_args(args)
48
+ if args.length > 0
49
+ return Source.from_pathlist(args)
50
+ else
51
+ return Source.from_io($stdin, 'stdin')
52
+ end
53
+ rescue OptionParser::ParseError, SystemCallError => err
54
+ fatal_error(err)
55
+ end
56
+ end
57
+
58
+ private
59
+ def self.set_output_format_option(p_config, p_opts)
60
+ p_opts.on('-of', '--output-format',
61
+ "Render output in the specified format, ie. html, " +
62
+ "by default 'text' is used.") do |value|
63
+ p_config[:output_format] = value
64
+ end
65
+ end
66
+
67
+ def self.set_version_option(opts)
68
+ opts.on("-v", "--version", "Show version") do
69
+ puts "#{opts.program_name} #{Reek::VERSION}"
70
+ exit(0)
71
+ end
72
+ end
73
+
74
+ def self.set_help_option(opts)
75
+ opts.on("-h", "--help", "Show this message") do
76
+ puts opts
77
+ exit(0)
78
+ end
79
+ end
80
+
81
+ def self.set_sort_option(config, opts)
82
+ opts.on('-f', "--format FORMAT", 'Specify the format of smell warnings') do |arg|
83
+ config[:format] = arg unless arg.nil?
84
+ end
85
+ opts.on('-c', '--context-first', "Sort by context; sets the format string to \"#{CTX_SORT}\"") do
86
+ config[:format] = CTX_SORT
87
+ end
88
+ opts.on('-s', '--smell-first', "Sort by smell; sets the format string to \"#{SMELL_SORT}\"") do
89
+ config[:format] = SMELL_SORT
90
+ end
91
+ end
92
+
93
+ def self.fatal_error(err) # :nodoc:
94
+ puts "Error: #{err}"
95
+ puts "Use '-h' for help."
96
+ exit(1)
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Define a task library for running reek.
4
+
5
+ require 'rake'
6
+ require 'rake/tasklib'
7
+
8
+ module Reek
9
+
10
+ # A Rake task that runs reek on a set of source files.
11
+ #
12
+ # Example:
13
+ #
14
+ # Reek::RakeTask.new do |t|
15
+ # t.fail_on_error = false
16
+ # end
17
+ #
18
+ # This will create a task that can be run with:
19
+ #
20
+ # rake reek
21
+ #
22
+ # Examples:
23
+ #
24
+ # rake reek # checks lib/**/*.rb
25
+ # rake reek REEK_SRC=just_one_file.rb # checks a single source file
26
+ # rake reek REEK_OPTS=-s # sorts the report by smell
27
+ #
28
+ class RakeTask < ::Rake::TaskLib
29
+
30
+ # Name of reek task.
31
+ # Defaults to :reek.
32
+ attr_accessor :name
33
+
34
+ # Array of directories to be added to $LOAD_PATH before running reek.
35
+ # Defaults to ['<the absolute path to reek's lib directory>']
36
+ attr_accessor :libs
37
+
38
+ # Glob pattern to match source files.
39
+ # Setting the REEK_SRC environment variable overrides this.
40
+ # Defaults to 'lib/**/*.rb'.
41
+ attr_accessor :source_files
42
+
43
+ # String containing commandline options to be passed to Reek.
44
+ # Setting the REEK_OPTS environment variable overrides this value.
45
+ # Defaults to ''.
46
+ attr_accessor :reek_opts
47
+
48
+ # Array of commandline options to pass to ruby. Defaults to [].
49
+ attr_accessor :ruby_opts
50
+
51
+ # Whether or not to fail Rake when an error occurs (typically when smells are found).
52
+ # Defaults to true.
53
+ attr_accessor :fail_on_error
54
+
55
+ # Use verbose output. If this is set to true, the task will print
56
+ # the reek command to stdout. Defaults to false.
57
+ attr_accessor :verbose
58
+
59
+ # Defines a new task, using the name +name+.
60
+ def initialize(name = :reek)
61
+ @name = name
62
+ @libs = [File.expand_path(File.dirname(__FILE__) + '/../../lib')]
63
+ @source_files = nil
64
+ @ruby_opts = []
65
+ @reek_opts = ''
66
+ @fail_on_error = true
67
+ @sort = nil
68
+
69
+ yield self if block_given?
70
+ @source_files ||= 'lib/**/*.rb'
71
+ define
72
+ end
73
+
74
+ private
75
+
76
+ def define # :nodoc:
77
+ desc 'Check for code smells' unless ::Rake.application.last_comment
78
+ task(name) { run_task }
79
+ self
80
+ end
81
+
82
+ def run_task
83
+ return if source_file_list.empty?
84
+ cmd = cmd_words.join(' ')
85
+ puts cmd if @verbose
86
+ raise('Smells found!') if !system(cmd) and fail_on_error
87
+ end
88
+
89
+ def self.reek_script
90
+ File.expand_path(File.dirname(__FILE__) + '/../../bin/reek')
91
+ end
92
+
93
+ def self.ruby_exe
94
+ File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])
95
+ end
96
+
97
+ def cmd_words
98
+ [RakeTask.ruby_exe] +
99
+ ruby_options +
100
+ [ %Q|"#{RakeTask.reek_script}"| ] +
101
+ [sort_option] +
102
+ source_file_list.collect { |fn| %["#{fn}"] }
103
+ end
104
+
105
+ def ruby_options
106
+ lib_path = @libs.join(File::PATH_SEPARATOR)
107
+ @ruby_opts.clone << "-I\"#{lib_path}\""
108
+ end
109
+
110
+ def sort_option
111
+ ENV['REEK_OPTS'] || @reek_opts
112
+ end
113
+
114
+ def source_file_list # :nodoc:
115
+ files = ENV['REEK_SRC'] || @source_files
116
+ return [] unless files
117
+ return FileList[files]
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,81 @@
1
+ require 'set'
2
+ require 'reek/smells/smell_detector'
3
+
4
+ module Reek
5
+ class Report
6
+ include Enumerable
7
+
8
+ def initialize # :nodoc:
9
+ @report = SortedSet.new
10
+ end
11
+
12
+ #
13
+ # Yields, in turn, each SmellWarning in this report.
14
+ #
15
+ def each
16
+ @report.each { |smell| yield smell }
17
+ end
18
+
19
+ def <<(smell) # :nodoc:
20
+ @report << smell
21
+ true
22
+ end
23
+
24
+ def empty?
25
+ @report.empty?
26
+ end
27
+
28
+ def length
29
+ @report.length
30
+ end
31
+
32
+ alias size length
33
+
34
+ def [](index) # :nodoc:
35
+ @report.to_a[index]
36
+ end
37
+
38
+ # Creates a formatted report of all the +Smells::SmellWarning+ objects recorded in
39
+ # this report, with a heading.
40
+ def full_report(desc)
41
+ "\"#{desc}\" -- #{length} warnings:\n#{to_s}\n"
42
+ end
43
+
44
+ # Creates a formatted report of all the +Smells::SmellWarning+ objects recorded in
45
+ # this report.
46
+ def to_s
47
+ @report.map {|smell| smell.report}.join("\n")
48
+ end
49
+ end
50
+
51
+ class ReportList
52
+ include Enumerable
53
+
54
+ def initialize(sources)
55
+ @sources = sources
56
+ end
57
+
58
+ #
59
+ # Yields, in turn, each SmellWarning in every report in this report.
60
+ #
61
+ def each(&blk)
62
+ @sources.each {|src| src.report.each(&blk) }
63
+ end
64
+
65
+ def empty?
66
+ length == 0
67
+ end
68
+
69
+ def length
70
+ @sources.inject(0) {|sum, src| sum + src.report.length }
71
+ end
72
+
73
+ def smelly_sources
74
+ @sources.select {|src| src.smelly? }
75
+ end
76
+
77
+ def to_s
78
+ smelly_sources.map { |src| src.full_report }.join("\n")
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,10 @@
1
+ require 'ruby2ruby'
2
+
3
+ module Reek
4
+ class SexpFormatter
5
+ def self.format(sexp)
6
+ sexp = YAML::load(YAML::dump(sexp))
7
+ Ruby2Ruby.new.process(sexp)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ require 'reek/name'
2
+ require 'reek/method_context'
3
+ require 'reek/sexp_formatter'
4
+
5
+ module Reek
6
+ class SingletonMethodContext < MethodContext
7
+
8
+ def initialize(outer, exp)
9
+ super(outer, exp, false)
10
+ @name = Name.new(exp[2])
11
+ @receiver = SexpFormatter.format(exp[1])
12
+ record_depends_on_self
13
+ end
14
+
15
+ def envious_receivers
16
+ []
17
+ end
18
+
19
+ def outer_name
20
+ "#{@outer.outer_name}#{@receiver}.#{@name}/"
21
+ end
22
+
23
+ def to_s
24
+ "#{@outer.outer_name}#{@receiver}.#{@name}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ require 'reek/options'
2
+
3
+ module Reek
4
+
5
+ #
6
+ # Reports a warning that a smell has been found.
7
+ #
8
+ class SmellWarning
9
+ include Comparable
10
+
11
+ def initialize(smell, context, warning)
12
+ @smell = smell
13
+ @context = context
14
+ @warning = warning
15
+ end
16
+
17
+ def hash # :nodoc:
18
+ report.hash
19
+ end
20
+
21
+ def <=>(other)
22
+ report <=> other.report
23
+ end
24
+
25
+ alias eql? <=> # :nodoc:
26
+
27
+ #
28
+ # Returns +true+ only if this is a warning about an instance of
29
+ # +smell_class+ and its report string matches all of the +patterns+.
30
+ #
31
+ def matches?(smell_class, patterns)
32
+ return false unless smell_class.to_s == @smell.class.class_name
33
+ rpt = report
34
+ return patterns.all? {|exp| exp === rpt}
35
+ end
36
+
37
+ #
38
+ # Returns a copy of the current report format (see +Options+)
39
+ # in which the following magic tokens have been substituted:
40
+ #
41
+ # * %s <-- the name of the smell that was detected
42
+ # * %c <-- a description of the +CodeContext+ containing the smell
43
+ # * %w <-- the specific problem that was detected
44
+ #
45
+ def report
46
+ Options[:format].gsub(/\%s/, @smell.smell_name).gsub(/\%c/, @context.to_s).gsub(/\%w/, @warning)
47
+ end
48
+ end
49
+ end