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
@@ -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
|