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
@@ -1,63 +1,17 @@
|
|
1
1
|
require_relative 'code_context'
|
2
|
-
require_relative '../ast/object_refs'
|
3
2
|
|
4
3
|
module Reek
|
5
4
|
module Context
|
6
|
-
#
|
7
|
-
# The parameters in a method's definition.
|
8
|
-
#
|
9
|
-
# @api private
|
10
|
-
module MethodParameters
|
11
|
-
def default_assignments
|
12
|
-
result = []
|
13
|
-
each do |exp|
|
14
|
-
result << exp[1..2] if exp.optional_argument?
|
15
|
-
end
|
16
|
-
result
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
5
|
#
|
21
6
|
# A context wrapper for any method definition found in a syntax tree.
|
22
7
|
#
|
23
8
|
# @api private
|
24
9
|
class MethodContext < CodeContext
|
25
|
-
attr_reader :parameters
|
26
10
|
attr_reader :refs
|
27
|
-
attr_reader :num_statements
|
28
|
-
|
29
|
-
def initialize(outer, exp)
|
30
|
-
super
|
31
|
-
@parameters = exp.parameters.dup
|
32
|
-
@parameters.extend MethodParameters
|
33
|
-
@num_statements = 0
|
34
|
-
@refs = AST::ObjectRefs.new
|
35
|
-
end
|
36
|
-
|
37
|
-
def count_statements(num)
|
38
|
-
@num_statements += num
|
39
|
-
end
|
40
|
-
|
41
|
-
def record_call_to(exp)
|
42
|
-
receiver, meth = exp[1..2]
|
43
|
-
receiver ||= [:self]
|
44
|
-
case receiver[0]
|
45
|
-
when :lvasgn
|
46
|
-
@refs.record_reference_to(receiver.name, line: exp.line)
|
47
|
-
when :lvar
|
48
|
-
@refs.record_reference_to(receiver.name, line: exp.line) unless meth == :new
|
49
|
-
when :self
|
50
|
-
@refs.record_reference_to(:self, line: exp.line)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def record_use_of_self
|
55
|
-
@refs.record_reference_to(:self)
|
56
|
-
end
|
57
11
|
|
58
12
|
def envious_receivers
|
59
|
-
return {} if
|
60
|
-
|
13
|
+
return {} if refs.self_is_max?
|
14
|
+
refs.most_popular
|
61
15
|
end
|
62
16
|
|
63
17
|
def references_self?
|
@@ -79,6 +33,15 @@ module Reek
|
|
79
33
|
def uses_super_with_implicit_arguments?
|
80
34
|
(body = exp.body) && body.contains_nested_node?(:zsuper)
|
81
35
|
end
|
36
|
+
|
37
|
+
def default_assignments
|
38
|
+
@default_assignments ||=
|
39
|
+
exp.parameters.select(&:optional_argument?).map(&:children)
|
40
|
+
end
|
41
|
+
|
42
|
+
def singleton_method?
|
43
|
+
exp.singleton_method? || visibility == :module_function
|
44
|
+
end
|
82
45
|
end
|
83
46
|
end
|
84
47
|
end
|
@@ -8,11 +8,6 @@ module Reek
|
|
8
8
|
#
|
9
9
|
# @api private
|
10
10
|
class ModuleContext < CodeContext
|
11
|
-
def initialize(outer, exp)
|
12
|
-
super(outer, exp)
|
13
|
-
@name = AST::SexpFormatter.format(exp.children.first)
|
14
|
-
end
|
15
|
-
|
16
11
|
def node_instance_methods
|
17
12
|
local_nodes(:def)
|
18
13
|
end
|
@@ -28,8 +23,9 @@ module Reek
|
|
28
23
|
#
|
29
24
|
# @return true if the module is a namespace module
|
30
25
|
def namespace_module?
|
26
|
+
return false if exp.type == :casgn
|
31
27
|
contents = exp.children.last
|
32
|
-
contents && contents.find_nodes([:def, :defs], [:class, :module]).empty?
|
28
|
+
contents && contents.find_nodes([:def, :defs], [:casgn, :class, :module]).empty?
|
33
29
|
end
|
34
30
|
end
|
35
31
|
end
|
@@ -1,25 +1,18 @@
|
|
1
|
+
require_relative 'code_context'
|
2
|
+
|
1
3
|
module Reek
|
2
|
-
# @api private
|
3
4
|
module Context
|
4
5
|
#
|
5
6
|
# A context wrapper representing the root of an abstract syntax tree.
|
6
7
|
#
|
7
8
|
# @api private
|
8
|
-
class RootContext
|
9
|
-
def initialize
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
def method_missing(_method, *_args)
|
14
|
-
nil
|
15
|
-
end
|
16
|
-
|
17
|
-
def config_for(_)
|
18
|
-
{}
|
9
|
+
class RootContext < CodeContext
|
10
|
+
def initialize(exp)
|
11
|
+
super(nil, exp)
|
19
12
|
end
|
20
13
|
|
21
|
-
def
|
22
|
-
|
14
|
+
def type
|
15
|
+
:root
|
23
16
|
end
|
24
17
|
|
25
18
|
def full_name
|
data/lib/reek/examiner.rb
CHANGED
@@ -21,13 +21,16 @@ module Reek
|
|
21
21
|
# If +source+ is a String it is assumed to be Ruby source code;
|
22
22
|
# if it is a File or IO, it is opened and Ruby source code is read from it;
|
23
23
|
#
|
24
|
-
# @param
|
24
|
+
# @param filter_by_smells [Array<String>]
|
25
25
|
# List of smell types to filter by.
|
26
26
|
#
|
27
|
-
def initialize(source,
|
28
|
-
|
29
|
-
|
30
|
-
@
|
27
|
+
def initialize(source,
|
28
|
+
filter_by_smells = [],
|
29
|
+
configuration: Configuration::AppConfiguration.new)
|
30
|
+
@source = Source::SourceCode.from(source)
|
31
|
+
@configuration = configuration
|
32
|
+
@collector = CLI::WarningCollector.new
|
33
|
+
@smell_types = eligible_smell_types(filter_by_smells)
|
31
34
|
|
32
35
|
run
|
33
36
|
end
|
@@ -63,16 +66,18 @@ module Reek
|
|
63
66
|
private
|
64
67
|
|
65
68
|
def run
|
66
|
-
smell_repository = Smells::SmellRepository.new(description,
|
69
|
+
smell_repository = Smells::SmellRepository.new(source_description: description,
|
70
|
+
smell_types: @smell_types,
|
71
|
+
configuration: @configuration)
|
67
72
|
syntax_tree = @source.syntax_tree
|
68
|
-
TreeWalker.new(smell_repository).
|
73
|
+
TreeWalker.new(smell_repository, syntax_tree).walk if syntax_tree
|
69
74
|
smell_repository.report_on(@collector)
|
70
75
|
end
|
71
76
|
|
72
|
-
def eligible_smell_types(
|
73
|
-
if
|
77
|
+
def eligible_smell_types(filter_by_smells = [])
|
78
|
+
if filter_by_smells.any?
|
74
79
|
Smells::SmellRepository.smell_types.select do |klass|
|
75
|
-
|
80
|
+
filter_by_smells.include? klass.smell_type
|
76
81
|
end
|
77
82
|
else
|
78
83
|
Smells::SmellRepository.smell_types
|
data/lib/reek/report/report.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'rainbow'
|
2
1
|
require 'json'
|
2
|
+
require 'pathname'
|
3
|
+
require 'rainbow'
|
3
4
|
require_relative 'formatter'
|
4
5
|
require_relative 'heading_formatter'
|
5
6
|
|
@@ -129,9 +130,9 @@ module Reek
|
|
129
130
|
class HTMLReport < Base
|
130
131
|
require 'erb'
|
131
132
|
|
132
|
-
def show(target_path = 'reek.html')
|
133
|
-
template_path =
|
134
|
-
File.write target_path, ERB.new(
|
133
|
+
def show(target_path = Pathname.new('reek.html'))
|
134
|
+
template_path = Pathname.new("#{__dir__}/html_report.html.erb")
|
135
|
+
File.write target_path, ERB.new(template_path.read).result(binding)
|
135
136
|
puts 'HTML file saved'
|
136
137
|
end
|
137
138
|
end
|
@@ -24,7 +24,7 @@ module Reek
|
|
24
24
|
# @return [Array<SmellWarning>]
|
25
25
|
#
|
26
26
|
def examine_context(method_ctx)
|
27
|
-
method_ctx.
|
27
|
+
method_ctx.default_assignments.select do |_param, value|
|
28
28
|
[:true, :false].include?(value[0])
|
29
29
|
end.map do |parameter, _value|
|
30
30
|
SmellWarning.new self,
|
@@ -108,7 +108,7 @@ module Reek
|
|
108
108
|
|
109
109
|
def collect_calls(result)
|
110
110
|
context.each_node(:send, [:mlhs]) do |call_node|
|
111
|
-
next if
|
111
|
+
next if call_node.object_creation_call?
|
112
112
|
next if simple_method_call? call_node
|
113
113
|
result[call_node].record(call_node)
|
114
114
|
end
|
@@ -125,10 +125,6 @@ module Reek
|
|
125
125
|
!call_node.receiver && call_node.args.empty?
|
126
126
|
end
|
127
127
|
|
128
|
-
def initializer_call?(call_node)
|
129
|
-
call_node.method_name == :new
|
130
|
-
end
|
131
|
-
|
132
128
|
def allow_calls?(method)
|
133
129
|
@allow_calls.any? { |allow| /#{allow}/ =~ method }
|
134
130
|
end
|
@@ -11,7 +11,7 @@ module Reek
|
|
11
11
|
# @api private
|
12
12
|
class IrresponsibleModule < SmellDetector
|
13
13
|
def self.contexts
|
14
|
-
[:class, :module]
|
14
|
+
[:casgn, :class, :module]
|
15
15
|
end
|
16
16
|
|
17
17
|
#
|
@@ -26,7 +26,7 @@ module Reek
|
|
26
26
|
context: ctx.full_name,
|
27
27
|
lines: [expression.line],
|
28
28
|
message: 'has no descriptive comment',
|
29
|
-
parameters: { name: expression.
|
29
|
+
parameters: { name: expression.name })]
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
@@ -9,45 +9,59 @@ module Reek
|
|
9
9
|
#
|
10
10
|
# @api private
|
11
11
|
class SmellRepository
|
12
|
-
attr_reader :detectors
|
13
|
-
|
14
12
|
def self.smell_types
|
15
13
|
Reek::Smells::SmellDetector.descendants.sort_by(&:name)
|
16
14
|
end
|
17
15
|
|
18
|
-
def initialize(source_description
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
def initialize(source_description: nil,
|
17
|
+
smell_types: self.class.smell_types,
|
18
|
+
configuration: Configuration::AppConfiguration.new)
|
19
|
+
self.source_via = source_description
|
20
|
+
self.typed_detectors = nil
|
21
|
+
self.configuration = configuration
|
22
|
+
self.smell_types = smell_types
|
23
|
+
|
24
|
+
configuration.directive_for(source_via).each do |klass, config|
|
25
|
+
configure klass, config
|
23
26
|
end
|
24
|
-
Configuration::AppConfiguration.configure_smell_repository self
|
25
27
|
end
|
26
28
|
|
27
29
|
def configure(klass, config)
|
28
|
-
detector =
|
30
|
+
detector = detectors[klass]
|
29
31
|
raise ArgumentError, "Unknown smell type #{klass} found in configuration" unless detector
|
30
32
|
detector.configure_with(config)
|
31
33
|
end
|
32
34
|
|
33
35
|
def report_on(listener)
|
34
|
-
|
36
|
+
detectors.each_value { |detector| detector.report_on(listener) }
|
35
37
|
end
|
36
38
|
|
37
|
-
def examine(scope
|
38
|
-
smell_listeners[
|
39
|
+
def examine(scope)
|
40
|
+
smell_listeners[scope.type].each do |detector|
|
39
41
|
detector.examine(scope)
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
45
|
+
def detectors
|
46
|
+
@initialized_detectors ||= begin
|
47
|
+
@detectors = {}
|
48
|
+
smell_types.each do |klass|
|
49
|
+
@detectors[klass] = klass.new(source_via)
|
50
|
+
end
|
51
|
+
@detectors
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
43
55
|
private
|
44
56
|
|
57
|
+
attr_accessor :typed_detectors, :configuration, :source_via, :smell_types
|
58
|
+
|
45
59
|
def smell_listeners
|
46
|
-
unless
|
47
|
-
|
48
|
-
|
60
|
+
unless typed_detectors
|
61
|
+
self.typed_detectors = Hash.new { |hash, key| hash[key] = [] }
|
62
|
+
detectors.each_value { |detector| detector.register(typed_detectors) }
|
49
63
|
end
|
50
|
-
|
64
|
+
typed_detectors
|
51
65
|
end
|
52
66
|
end
|
53
67
|
end
|
@@ -54,7 +54,7 @@ module Reek
|
|
54
54
|
# @return [Array<SmellWarning>]
|
55
55
|
#
|
56
56
|
def examine_context(method_ctx)
|
57
|
-
return [] if method_ctx.
|
57
|
+
return [] if method_ctx.singleton_method?
|
58
58
|
return [] if method_ctx.num_statements == 0
|
59
59
|
return [] if method_ctx.references_self?
|
60
60
|
return [] if num_helper_methods(method_ctx).zero?
|
@@ -13,12 +13,15 @@ module Reek
|
|
13
13
|
#
|
14
14
|
# @api private
|
15
15
|
class SourceCode
|
16
|
+
IO_IDENTIFIER = 'STDIN'
|
17
|
+
STRING_IDENTIFIER = 'string'
|
18
|
+
|
16
19
|
attr_reader :description
|
17
20
|
|
18
21
|
# Initializer.
|
19
22
|
#
|
20
23
|
# code - ruby code as String
|
21
|
-
# description -
|
24
|
+
# description - 'STDIN', 'string' or a filepath as String
|
22
25
|
# parser - the parser to use for generating AST's out of the given source
|
23
26
|
def initialize(code, description, parser = Parser::Ruby22)
|
24
27
|
@source = code
|
@@ -27,8 +30,8 @@ module Reek
|
|
27
30
|
end
|
28
31
|
|
29
32
|
# Initializes an instance of SourceCode given a source.
|
30
|
-
# This source can come via
|
31
|
-
# - from
|
33
|
+
# This source can come via 4 different ways:
|
34
|
+
# - from Files or Pathnames a la `reek lib/reek/`
|
32
35
|
# - from IO (STDIN) a la `echo "class Foo; end" | reek`
|
33
36
|
# - from String via our rspec matchers a la `expect("class Foo; end").to reek`
|
34
37
|
#
|
@@ -37,9 +40,10 @@ module Reek
|
|
37
40
|
# @return an instance of SourceCode
|
38
41
|
def self.from(source)
|
39
42
|
case source
|
40
|
-
when File
|
41
|
-
when IO
|
42
|
-
when
|
43
|
+
when File then new(source.read, source.path)
|
44
|
+
when IO then new(source.readlines.join, IO_IDENTIFIER)
|
45
|
+
when Pathname then new(source.read, source.to_s)
|
46
|
+
when String then new(source, STRING_IDENTIFIER)
|
43
47
|
end
|
44
48
|
end
|
45
49
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'find'
|
2
|
+
require 'pathname'
|
2
3
|
|
3
4
|
module Reek
|
4
5
|
module Source
|
@@ -10,61 +11,61 @@ module Reek
|
|
10
11
|
# Initialize with the paths we want to search.
|
11
12
|
#
|
12
13
|
# paths - a list of paths as Strings
|
13
|
-
def initialize(paths)
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
def initialize(paths, configuration: Configuration::AppConfiguration.new)
|
15
|
+
self.paths = paths.flat_map do |string|
|
16
|
+
path = Pathname.new(string)
|
17
|
+
current_directory?(path) ? path.entries : path
|
18
|
+
end
|
19
|
+
self.configuration = configuration
|
17
20
|
end
|
18
21
|
|
19
22
|
# Traverses all paths we initialized the SourceLocator with, finds
|
20
23
|
# all relevant ruby files and returns them as a list.
|
21
24
|
#
|
22
|
-
# @return [Array<
|
25
|
+
# @return [Array<Pathname>] - Ruby paths found
|
23
26
|
def sources
|
24
|
-
source_paths
|
27
|
+
source_paths
|
25
28
|
end
|
26
29
|
|
27
30
|
private
|
28
31
|
|
32
|
+
attr_accessor :paths, :configuration
|
33
|
+
|
29
34
|
def source_paths
|
30
|
-
|
31
|
-
print_no_such_file_error(
|
32
|
-
|
33
|
-
if
|
34
|
-
ignore_path?(
|
35
|
+
paths.each_with_object([]) do |given_path, relevant_paths|
|
36
|
+
print_no_such_file_error(given_path) && next unless given_path.exist?
|
37
|
+
given_path.find do |path|
|
38
|
+
if path.directory?
|
39
|
+
ignore_path?(path) ? Find.prune : next
|
35
40
|
else
|
36
|
-
relevant_paths <<
|
41
|
+
relevant_paths << path if ruby_file?(path)
|
37
42
|
end
|
38
43
|
end
|
39
44
|
end
|
40
45
|
end
|
41
46
|
|
42
|
-
def path_excluded?(
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
def path_exists?(path)
|
47
|
-
Pathname.new(path).exist?
|
47
|
+
def path_excluded?(path)
|
48
|
+
configuration.exclude_paths.include?(path)
|
48
49
|
end
|
49
50
|
|
50
51
|
def print_no_such_file_error(path)
|
51
52
|
$stderr.puts "Error: No such file - #{path}"
|
52
53
|
end
|
53
54
|
|
54
|
-
def hidden_directory?(
|
55
|
-
|
55
|
+
def hidden_directory?(path)
|
56
|
+
path.basename.to_s.start_with? '.'
|
56
57
|
end
|
57
58
|
|
58
|
-
def ignore_path?(
|
59
|
-
path_excluded?(
|
59
|
+
def ignore_path?(path)
|
60
|
+
path_excluded?(path) || hidden_directory?(path)
|
60
61
|
end
|
61
62
|
|
62
|
-
def ruby_file?(
|
63
|
-
|
63
|
+
def ruby_file?(path)
|
64
|
+
path.extname == '.rb'
|
64
65
|
end
|
65
66
|
|
66
|
-
def current_directory?(
|
67
|
-
['.', './'].include?
|
67
|
+
def current_directory?(path)
|
68
|
+
[Pathname.new('.'), Pathname.new('./')].include?(path)
|
68
69
|
end
|
69
70
|
end
|
70
71
|
end
|