reek 3.0.4 → 3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +6 -0
- data/README.md +41 -20
- data/features/configuration_files/directory_specific_directives.feature +280 -0
- data/features/configuration_files/masking_smells.feature +0 -14
- data/features/step_definitions/sample_file_steps.rb +2 -2
- data/lib/reek/ast/sexp_extensions.rb +72 -60
- data/lib/reek/cli/application.rb +2 -5
- data/lib/reek/cli/reek_command.rb +1 -1
- data/lib/reek/configuration/app_configuration.rb +115 -61
- data/lib/reek/configuration/configuration_file_finder.rb +19 -0
- data/lib/reek/context/code_context.rb +102 -29
- data/lib/reek/context/method_context.rb +11 -48
- data/lib/reek/context/module_context.rb +2 -6
- data/lib/reek/context/root_context.rb +7 -14
- data/lib/reek/examiner.rb +15 -10
- data/lib/reek/report/report.rb +5 -4
- data/lib/reek/smells/boolean_parameter.rb +1 -1
- data/lib/reek/smells/duplicate_method_call.rb +1 -5
- data/lib/reek/smells/irresponsible_module.rb +2 -2
- data/lib/reek/smells/smell_repository.rb +30 -16
- data/lib/reek/smells/utility_function.rb +1 -1
- data/lib/reek/source/source_code.rb +10 -6
- data/lib/reek/source/source_locator.rb +27 -26
- data/lib/reek/spec.rb +8 -6
- data/lib/reek/spec/should_reek.rb +6 -2
- data/lib/reek/spec/should_reek_of.rb +5 -2
- data/lib/reek/spec/should_reek_only_of.rb +1 -1
- data/lib/reek/tree_walker.rb +49 -21
- data/lib/reek/version.rb +1 -1
- data/spec/reek/ast/sexp_extensions_spec.rb +4 -12
- data/spec/reek/configuration/app_configuration_spec.rb +80 -52
- data/spec/reek/configuration/configuration_file_finder_spec.rb +27 -15
- data/spec/reek/context/code_context_spec.rb +66 -17
- data/spec/reek/context/method_context_spec.rb +66 -64
- data/spec/reek/context/root_context_spec.rb +3 -1
- data/spec/reek/context/singleton_method_context_spec.rb +1 -2
- data/spec/reek/examiner_spec.rb +5 -8
- data/spec/reek/report/xml_report_spec.rb +3 -2
- data/spec/reek/smells/attribute_spec.rb +12 -17
- data/spec/reek/smells/duplicate_method_call_spec.rb +17 -25
- data/spec/reek/smells/feature_envy_spec.rb +4 -5
- data/spec/reek/smells/irresponsible_module_spec.rb +43 -0
- data/spec/reek/smells/nested_iterators_spec.rb +12 -26
- data/spec/reek/smells/too_many_statements_spec.rb +2 -210
- data/spec/reek/smells/utility_function_spec.rb +49 -5
- data/spec/reek/source/source_locator_spec.rb +39 -43
- data/spec/reek/spec/should_reek_of_spec.rb +3 -2
- data/spec/reek/spec/should_reek_spec.rb +25 -17
- data/spec/reek/tree_walker_spec.rb +214 -23
- data/spec/samples/configuration/full_configuration.reek +9 -0
- data/spec/spec_helper.rb +10 -10
- metadata +4 -3
- data/features/configuration_files/overrides_defaults.feature +0 -15
data/lib/reek/cli/application.rb
CHANGED
@@ -14,6 +14,7 @@ module Reek
|
|
14
14
|
STATUS_SUCCESS = 0
|
15
15
|
STATUS_ERROR = 1
|
16
16
|
STATUS_SMELLS = 2
|
17
|
+
attr_reader :configuration
|
17
18
|
|
18
19
|
def initialize(argv)
|
19
20
|
@status = STATUS_SUCCESS
|
@@ -21,7 +22,7 @@ module Reek
|
|
21
22
|
begin
|
22
23
|
@options = options_parser.parse
|
23
24
|
@command = ReekCommand.new(OptionInterpreter.new(@options))
|
24
|
-
|
25
|
+
@configuration = Configuration::AppConfiguration.new @options
|
25
26
|
rescue OptionParser::InvalidOption, Reek::Configuration::ConfigFileException => error
|
26
27
|
$stderr.puts "Error: #{error}"
|
27
28
|
@status = STATUS_ERROR
|
@@ -34,10 +35,6 @@ module Reek
|
|
34
35
|
@status
|
35
36
|
end
|
36
37
|
|
37
|
-
def initialize_configuration
|
38
|
-
Configuration::AppConfiguration.initialize_with @options
|
39
|
-
end
|
40
|
-
|
41
38
|
def output(text)
|
42
39
|
print text
|
43
40
|
end
|
@@ -11,7 +11,7 @@ module Reek
|
|
11
11
|
class ReekCommand < Command
|
12
12
|
def execute(app)
|
13
13
|
@options.sources.each do |source|
|
14
|
-
reporter.add_examiner Examiner.new(source, smell_names)
|
14
|
+
reporter.add_examiner Examiner.new(source, smell_names, configuration: app.configuration)
|
15
15
|
end
|
16
16
|
reporter.smells? ? app.report_smells : app.report_success
|
17
17
|
reporter.show
|
@@ -1,90 +1,144 @@
|
|
1
|
+
require 'pathname'
|
1
2
|
require_relative './configuration_file_finder'
|
2
3
|
|
3
4
|
module Reek
|
4
5
|
# @api private
|
5
6
|
module Configuration
|
6
7
|
# @api private
|
7
|
-
class ConfigFileException < StandardError; end
|
8
8
|
#
|
9
9
|
# Reek's singleton configuration instance.
|
10
10
|
#
|
11
11
|
# @api private
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@configuration = {}
|
16
|
-
@has_been_initialized = false
|
17
|
-
|
18
|
-
class << self
|
19
|
-
attr_reader :configuration
|
20
|
-
|
21
|
-
def initialize_with(options)
|
22
|
-
@has_been_initialized = true
|
23
|
-
configuration_file_path = ConfigurationFileFinder.find(options: options)
|
24
|
-
return unless configuration_file_path
|
25
|
-
load_from_file configuration_file_path
|
26
|
-
end
|
12
|
+
class AppConfiguration
|
13
|
+
EXCLUDE_PATHS_KEY = 'exclude_paths'
|
14
|
+
attr_reader :exclude_paths, :default_directive, :directory_directives
|
27
15
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
16
|
+
# Given this configuration file:
|
17
|
+
#
|
18
|
+
# ---
|
19
|
+
# IrresponsibleModule:
|
20
|
+
# enabled: false
|
21
|
+
# "app/helpers":
|
22
|
+
# UtilityFunction:
|
23
|
+
# enabled: false
|
24
|
+
# exclude_paths:
|
25
|
+
# - "app/controllers"
|
26
|
+
#
|
27
|
+
# this would result in the following configuration:
|
28
|
+
#
|
29
|
+
# exclude_paths = [ Pathname('spec/samples/two_smelly_files') ]
|
30
|
+
# default_directive = { Reek::Smells::IrresponsibleModule => { "enabled" => false } }
|
31
|
+
# directory_directives = { Pathname("spec/samples/three_clean_files/") =>
|
32
|
+
# { Reek::Smells::UtilityFunction => { "enabled" => false } } }
|
33
|
+
#
|
34
|
+
#
|
35
|
+
# @param options [OpenStruct]
|
36
|
+
# e.g.: #<OpenStruct report_format =: text,
|
37
|
+
# location_format =: numbers,
|
38
|
+
# colored = true,
|
39
|
+
# smells_to_detect = [],
|
40
|
+
# config_file = #<Pathname:config/defaults.reek>,
|
41
|
+
# argv = [ "lib/reek/spec" ]>
|
42
|
+
def initialize(options = nil)
|
43
|
+
self.directory_directives = {}
|
44
|
+
self.default_directive = {}
|
45
|
+
self.exclude_paths = []
|
37
46
|
|
38
|
-
|
39
|
-
|
40
|
-
report_problem('Empty file', path)
|
41
|
-
return
|
42
|
-
end
|
47
|
+
load options
|
48
|
+
end
|
43
49
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
50
|
+
# @param source_via [String] - the source of the code inspected
|
51
|
+
# @return [Hash] the directory directive for the source or, if there is
|
52
|
+
# none, the default directive
|
53
|
+
def directive_for(source_via)
|
54
|
+
directory_directive_for_source(source_via) || default_directive
|
55
|
+
end
|
49
56
|
|
50
|
-
|
51
|
-
end
|
57
|
+
private
|
52
58
|
|
53
|
-
|
54
|
-
@configuration.clear
|
55
|
-
end
|
59
|
+
attr_writer :exclude_paths, :default_directive, :directory_directives
|
56
60
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
61
|
+
# @param source_via [String] - the source of the code inspected
|
62
|
+
# Might be a string, STDIN or Filename / Pathname. We're only interested in the source
|
63
|
+
# when it's coming from file since a directory_directive doesn't make sense
|
64
|
+
# for anything else.
|
65
|
+
# @return [Hash | nil] the configuration for the source or nil
|
66
|
+
def directory_directive_for_source(source_via)
|
67
|
+
return unless source_via
|
68
|
+
source_base_dir = Pathname.new(source_via).dirname
|
69
|
+
hit = best_directory_match_for source_base_dir
|
70
|
+
directory_directives[hit]
|
71
|
+
end
|
62
72
|
|
63
|
-
|
73
|
+
def load(options)
|
74
|
+
configuration_file = ConfigurationFileFinder.find_and_load(options: options)
|
64
75
|
|
65
|
-
|
66
|
-
|
76
|
+
configuration_file.each do |key, value|
|
77
|
+
case
|
78
|
+
when key == EXCLUDE_PATHS_KEY
|
79
|
+
handle_exclude_paths(value)
|
80
|
+
when smell_type?(key)
|
81
|
+
handle_default_directive(key, value)
|
82
|
+
else
|
83
|
+
handle_directory_directive(key, value)
|
84
|
+
end
|
67
85
|
end
|
86
|
+
end
|
68
87
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
88
|
+
def handle_exclude_paths(paths)
|
89
|
+
self.exclude_paths = paths.map do |path|
|
90
|
+
pathname = Pathname.new path.chomp('/')
|
91
|
+
raise ArgumentError, "Excluded directory #{path} does not exists" unless pathname.exist?
|
92
|
+
pathname
|
74
93
|
end
|
94
|
+
end
|
75
95
|
|
76
|
-
|
77
|
-
|
78
|
-
|
96
|
+
def handle_default_directive(key, config)
|
97
|
+
klass = Reek::Smells.const_get(key)
|
98
|
+
default_directive[klass] = config
|
99
|
+
end
|
79
100
|
|
80
|
-
|
81
|
-
|
82
|
-
|
101
|
+
def handle_directory_directive(path, config)
|
102
|
+
pathname = Pathname.new path.chomp('/')
|
103
|
+
validate_directive pathname
|
83
104
|
|
84
|
-
|
85
|
-
|
105
|
+
directory_directives[pathname] = config.each_with_object({}) do |(key, value), hash|
|
106
|
+
abort(error_message_for_invalid_smell_type(key)) unless smell_type?(key)
|
107
|
+
hash[Reek::Smells.const_get(key)] = value
|
86
108
|
end
|
87
109
|
end
|
110
|
+
|
111
|
+
def best_directory_match_for(source_base_dir)
|
112
|
+
directory_directives.
|
113
|
+
keys.
|
114
|
+
select { |pathname| source_base_dir.to_s =~ /#{pathname}/ }.
|
115
|
+
max_by { |pathname| pathname.to_s.length }
|
116
|
+
end
|
117
|
+
|
118
|
+
def smell_type?(key)
|
119
|
+
Reek::Smells.const_get key
|
120
|
+
rescue NameError
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
124
|
+
def error_message_for_invalid_smell_type(klass)
|
125
|
+
"You are trying to configure smell type #{klass} but we can't find one with that name.\n" \
|
126
|
+
"Please make sure you spelled it right (see 'config/defaults.reek' in the reek\n" \
|
127
|
+
'repository for a list of all available smell types.'
|
128
|
+
end
|
129
|
+
|
130
|
+
def error_message_for_missing_directory(pathname)
|
131
|
+
"Configuration error: Directory `#{pathname}` does not exist"
|
132
|
+
end
|
133
|
+
|
134
|
+
def error_message_for_file_given(pathname)
|
135
|
+
"Configuration error: `#{pathname}` is supposed to be a directory but is a file"
|
136
|
+
end
|
137
|
+
|
138
|
+
def validate_directive(pathname)
|
139
|
+
abort(error_message_for_missing_directory(pathname)) unless pathname.exist?
|
140
|
+
abort(error_message_for_file_given(pathname)) unless pathname.directory?
|
141
|
+
end
|
88
142
|
end
|
89
143
|
end
|
90
144
|
end
|
@@ -2,6 +2,7 @@ require 'pathname'
|
|
2
2
|
|
3
3
|
module Reek
|
4
4
|
module Configuration
|
5
|
+
class ConfigFileException < StandardError; end
|
5
6
|
#
|
6
7
|
# ConfigurationFileFinder is responsible for finding reek's configuration.
|
7
8
|
#
|
@@ -17,6 +18,10 @@ module Reek
|
|
17
18
|
module ConfigurationFileFinder
|
18
19
|
module_function
|
19
20
|
|
21
|
+
def find_and_load(params = {})
|
22
|
+
load_from_file(find(params))
|
23
|
+
end
|
24
|
+
|
20
25
|
# FIXME: switch to kwargs on upgrade to Ruby 2 and drop `params.fetch` calls:
|
21
26
|
# def find(options: nil, current: Pathname.pwd, home: Pathname.new(Dir.home))
|
22
27
|
def find(params = {})
|
@@ -37,6 +42,20 @@ module Reek
|
|
37
42
|
return found if found
|
38
43
|
end
|
39
44
|
end
|
45
|
+
|
46
|
+
def load_from_file(path)
|
47
|
+
return {} unless path
|
48
|
+
begin
|
49
|
+
configuration = YAML.load_file(path) || {}
|
50
|
+
rescue => error
|
51
|
+
raise ConfigFileException, "Invalid configuration file #{path}, error is #{error}"
|
52
|
+
end
|
53
|
+
|
54
|
+
unless configuration.is_a? Hash
|
55
|
+
raise ConfigFileException, "Invalid configuration file \"#{path}\" -- Not a hash"
|
56
|
+
end
|
57
|
+
configuration
|
58
|
+
end
|
40
59
|
end
|
41
60
|
end
|
42
61
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require_relative '../code_comment'
|
2
|
+
require_relative '../ast/object_refs'
|
2
3
|
|
3
4
|
module Reek
|
5
|
+
# @api private
|
4
6
|
module Context
|
5
7
|
#
|
6
8
|
# Superclass for all types of source code context. Each instance represents
|
@@ -11,23 +13,27 @@ module Reek
|
|
11
13
|
# @api private
|
12
14
|
class CodeContext
|
13
15
|
attr_reader :exp
|
16
|
+
attr_reader :num_statements
|
17
|
+
attr_reader :children
|
18
|
+
attr_reader :visibility
|
14
19
|
|
15
20
|
# Initializes a new CodeContext.
|
16
21
|
#
|
17
|
-
# context
|
18
|
-
# exp
|
22
|
+
# @param context [CodeContext, nil] The parent context
|
23
|
+
# @param exp [Reek::AST::Node] The code described by this context
|
19
24
|
#
|
20
|
-
#
|
25
|
+
# For example, given the following code:
|
21
26
|
#
|
22
|
-
#
|
27
|
+
# class Omg
|
28
|
+
# def foo(x)
|
29
|
+
# puts x
|
30
|
+
# end
|
31
|
+
# end
|
23
32
|
#
|
24
|
-
#
|
33
|
+
# The {TreeWalker} object first instantiates a {RootContext}, which has no parent.
|
25
34
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# #<Reek::Context::RootContext:0x00000002231098 @name="">
|
29
|
-
#
|
30
|
-
# and `exp` looks like this:
|
35
|
+
# Next, it instantiates a {ModuleContext}, with +context+ being the
|
36
|
+
# {RootContext} just created, and +exp+ looking like this:
|
31
37
|
#
|
32
38
|
# (class
|
33
39
|
# (const nil :Omg) nil
|
@@ -37,20 +43,55 @@ module Reek
|
|
37
43
|
# (send nil :puts
|
38
44
|
# (lvar :x))))
|
39
45
|
#
|
40
|
-
#
|
46
|
+
# Finally, {TreeWalker} will instantiate a {MethodContext}. This time,
|
47
|
+
# +context+ is the {ModuleContext} created above, and +exp+ is:
|
41
48
|
#
|
42
|
-
#
|
49
|
+
# (def :foo
|
50
|
+
# (args
|
51
|
+
# (arg :x))
|
52
|
+
# (send nil :puts
|
53
|
+
# (lvar :x)))
|
54
|
+
def initialize(context, exp)
|
55
|
+
@context = context
|
56
|
+
@exp = exp
|
57
|
+
@visibility = :public
|
58
|
+
@children = []
|
59
|
+
|
60
|
+
@num_statements = 0
|
61
|
+
@refs = AST::ObjectRefs.new
|
62
|
+
end
|
63
|
+
|
64
|
+
# Register a child context. The child's parent context should be equal to
|
65
|
+
# the current context.
|
43
66
|
#
|
44
|
-
#
|
67
|
+
# This makes the current context responsible for setting the child's
|
68
|
+
# visibility.
|
45
69
|
#
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
@
|
70
|
+
# @param child [CodeContext] the child context to register
|
71
|
+
def append_child_context(child)
|
72
|
+
child.visibility = tracked_visibility
|
73
|
+
@children << child
|
74
|
+
end
|
75
|
+
|
76
|
+
def count_statements(num)
|
77
|
+
@num_statements += num
|
78
|
+
end
|
79
|
+
|
80
|
+
def record_call_to(exp)
|
81
|
+
receiver = exp.receiver
|
82
|
+
type = receiver ? receiver.type : :self
|
83
|
+
case type
|
84
|
+
when :lvar, :lvasgn
|
85
|
+
unless exp.object_creation_call?
|
86
|
+
@refs.record_reference_to(receiver.name, line: exp.line)
|
87
|
+
end
|
88
|
+
when :self
|
89
|
+
@refs.record_reference_to(:self, line: exp.line)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def record_use_of_self
|
94
|
+
@refs.record_reference_to(:self)
|
54
95
|
end
|
55
96
|
|
56
97
|
def name
|
@@ -73,14 +114,6 @@ module Reek
|
|
73
114
|
end
|
74
115
|
end
|
75
116
|
|
76
|
-
#
|
77
|
-
# Bounces messages up the context tree to the first enclosing context
|
78
|
-
# that knows how to deal with the request.
|
79
|
-
#
|
80
|
-
def method_missing(method, *args)
|
81
|
-
@context.send(method, *args)
|
82
|
-
end
|
83
|
-
|
84
117
|
def num_methods
|
85
118
|
0
|
86
119
|
end
|
@@ -95,8 +128,48 @@ module Reek
|
|
95
128
|
config[detector_class.smell_type] || {})
|
96
129
|
end
|
97
130
|
|
131
|
+
# Handle the effects of a visibility modifier.
|
132
|
+
#
|
133
|
+
# @example Setting the current visibility
|
134
|
+
# track_visibility :public
|
135
|
+
#
|
136
|
+
# @example Modifying the visibility of existing children
|
137
|
+
# track_visibility :private, [:hide_me, :implementation_detail]
|
138
|
+
#
|
139
|
+
# @param visibility [Symbol]
|
140
|
+
# @param names [Array<Symbol>]
|
141
|
+
def track_visibility(visibility, names = [])
|
142
|
+
if names.any?
|
143
|
+
@children.each do |child|
|
144
|
+
child.visibility = visibility if names.include? child.name
|
145
|
+
end
|
146
|
+
else
|
147
|
+
@tracked_visibility = visibility
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def type
|
152
|
+
@exp.type
|
153
|
+
end
|
154
|
+
|
155
|
+
# Iterate over +self+ and child contexts.
|
156
|
+
def each(&block)
|
157
|
+
yield self
|
158
|
+
@children.each do |child|
|
159
|
+
child.each(&block)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
attr_writer :visibility
|
166
|
+
|
98
167
|
private
|
99
168
|
|
169
|
+
def tracked_visibility
|
170
|
+
@tracked_visibility ||= :public
|
171
|
+
end
|
172
|
+
|
100
173
|
def config
|
101
174
|
@config ||= if @exp
|
102
175
|
CodeComment.new(@exp.full_comment || '').config
|