reek 3.0.4 → 3.1
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.
- 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
|