improve_your_code 1.0.0
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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +12 -0
- data/README.md +0 -0
- data/Rakefile +4 -0
- data/bin/improve_your_code +6 -0
- data/improve_your_code.gemspec +22 -0
- data/lib/improve_your_code/ast/ast_node_class_map.rb +39 -0
- data/lib/improve_your_code/ast/builder.rb +11 -0
- data/lib/improve_your_code/ast/node.rb +108 -0
- data/lib/improve_your_code/ast/object_refs.rb +32 -0
- data/lib/improve_your_code/ast/reference_collector.rb +29 -0
- data/lib/improve_your_code/ast/sexp_extensions.rb +15 -0
- data/lib/improve_your_code/ast/sexp_extensions/arguments.rb +89 -0
- data/lib/improve_your_code/ast/sexp_extensions/block.rb +38 -0
- data/lib/improve_your_code/ast/sexp_extensions/constant.rb +13 -0
- data/lib/improve_your_code/ast/sexp_extensions/if.rb +18 -0
- data/lib/improve_your_code/ast/sexp_extensions/methods.rb +81 -0
- data/lib/improve_your_code/ast/sexp_extensions/module.rb +64 -0
- data/lib/improve_your_code/ast/sexp_extensions/nested_assignables.rb +17 -0
- data/lib/improve_your_code/ast/sexp_extensions/self.rb +13 -0
- data/lib/improve_your_code/ast/sexp_extensions/send.rb +55 -0
- data/lib/improve_your_code/ast/sexp_extensions/symbols.rb +14 -0
- data/lib/improve_your_code/ast/sexp_extensions/variables.rb +45 -0
- data/lib/improve_your_code/cli/application.rb +32 -0
- data/lib/improve_your_code/cli/command/report_command.rb +39 -0
- data/lib/improve_your_code/cli/silencer.rb +24 -0
- data/lib/improve_your_code/code_comment.rb +52 -0
- data/lib/improve_your_code/context/attribute_context.rb +33 -0
- data/lib/improve_your_code/context/code_context.rb +121 -0
- data/lib/improve_your_code/context/method_context.rb +79 -0
- data/lib/improve_your_code/context/module_context.rb +77 -0
- data/lib/improve_your_code/context/root_context.rb +22 -0
- data/lib/improve_your_code/context/send_context.rb +16 -0
- data/lib/improve_your_code/context/singleton_attribute_context.rb +13 -0
- data/lib/improve_your_code/context/singleton_method_context.rb +29 -0
- data/lib/improve_your_code/context/statement_counter.rb +27 -0
- data/lib/improve_your_code/context/visibility_tracker.rb +52 -0
- data/lib/improve_your_code/context_builder.rb +121 -0
- data/lib/improve_your_code/detector_repository.rb +32 -0
- data/lib/improve_your_code/examiner.rb +48 -0
- data/lib/improve_your_code/report/formatter.rb +25 -0
- data/lib/improve_your_code/report/formatter/heading_formatter.rb +33 -0
- data/lib/improve_your_code/report/formatter/progress_formatter.rb +25 -0
- data/lib/improve_your_code/report/formatter/simple_warning_formatter.rb +13 -0
- data/lib/improve_your_code/report/text_report.rb +76 -0
- data/lib/improve_your_code/smell_configuration.rb +48 -0
- data/lib/improve_your_code/smell_detectors.rb +12 -0
- data/lib/improve_your_code/smell_detectors/base_detector.rb +114 -0
- data/lib/improve_your_code/smell_detectors/long_parameter_list.rb +44 -0
- data/lib/improve_your_code/smell_detectors/too_many_constants.rb +54 -0
- data/lib/improve_your_code/smell_detectors/too_many_instance_variables.rb +48 -0
- data/lib/improve_your_code/smell_detectors/too_many_methods.rb +47 -0
- data/lib/improve_your_code/smell_detectors/too_many_statements.rb +43 -0
- data/lib/improve_your_code/smell_detectors/uncommunicative_method_name.rb +58 -0
- data/lib/improve_your_code/smell_detectors/uncommunicative_module_name.rb +71 -0
- data/lib/improve_your_code/smell_detectors/uncommunicative_variable_name.rb +129 -0
- data/lib/improve_your_code/smell_detectors/unused_parameters.rb +24 -0
- data/lib/improve_your_code/smell_detectors/unused_private_method.rb +69 -0
- data/lib/improve_your_code/smell_warning.rb +70 -0
- data/lib/improve_your_code/source/source_code.rb +84 -0
- data/lib/improve_your_code/source/source_locator.rb +38 -0
- data/lib/improve_your_code/tree_dresser.rb +74 -0
- data/lib/improve_your_code/version.rb +7 -0
- metadata +141 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require_relative '../smell_warning'
|
5
|
+
require_relative '../smell_configuration'
|
6
|
+
|
7
|
+
module ImproveYourCode
|
8
|
+
module SmellDetectors
|
9
|
+
class BaseDetector
|
10
|
+
attr_reader :config
|
11
|
+
|
12
|
+
EXCLUDE_KEY = 'exclude'
|
13
|
+
|
14
|
+
def initialize(context:)
|
15
|
+
@config = SmellConfiguration.new(self.class.default_config)
|
16
|
+
@context = context
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.todo_configuration_for(smells)
|
20
|
+
default_exclusions = default_config.fetch EXCLUDE_KEY
|
21
|
+
exclusions = default_exclusions + smells.map(&:context)
|
22
|
+
|
23
|
+
{ smell_type => { EXCLUDE_KEY => exclusions.uniq } }
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
sniff
|
28
|
+
end
|
29
|
+
|
30
|
+
def smell_type
|
31
|
+
self.class.smell_type
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :context
|
37
|
+
|
38
|
+
def config_for(ctx)
|
39
|
+
ctx.config_for(self.class)
|
40
|
+
end
|
41
|
+
|
42
|
+
def enabled?
|
43
|
+
config.enabled? && config_for(context)[
|
44
|
+
SmellConfiguration::ENABLED_KEY
|
45
|
+
] != false
|
46
|
+
end
|
47
|
+
|
48
|
+
def exception?
|
49
|
+
context.matches?(value(EXCLUDE_KEY, context))
|
50
|
+
end
|
51
|
+
|
52
|
+
def expression
|
53
|
+
@expression ||= context.exp
|
54
|
+
end
|
55
|
+
|
56
|
+
def smell_warning(options = {})
|
57
|
+
context = options.fetch(:context)
|
58
|
+
exp = context.exp
|
59
|
+
SmellWarning.new(self,
|
60
|
+
source: exp.source,
|
61
|
+
context: context.full_name,
|
62
|
+
lines: options.fetch(:lines),
|
63
|
+
message: options.fetch(:message),
|
64
|
+
parameters: options.fetch(:parameters, {}))
|
65
|
+
end
|
66
|
+
|
67
|
+
def source_line
|
68
|
+
@line ||= expression.line
|
69
|
+
end
|
70
|
+
|
71
|
+
def value(key, ctx)
|
72
|
+
config_for(ctx)[key] || config.value(key, ctx)
|
73
|
+
end
|
74
|
+
|
75
|
+
class << self
|
76
|
+
def smell_type
|
77
|
+
@smell_type ||= name.split(/::/).last
|
78
|
+
end
|
79
|
+
|
80
|
+
def contexts
|
81
|
+
%i[def defs]
|
82
|
+
end
|
83
|
+
|
84
|
+
def default_config
|
85
|
+
{
|
86
|
+
SmellConfiguration::ENABLED_KEY => true,
|
87
|
+
EXCLUDE_KEY => []
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def inherited(subclass)
|
92
|
+
descendants << subclass
|
93
|
+
end
|
94
|
+
|
95
|
+
def descendants
|
96
|
+
@descendants ||= []
|
97
|
+
end
|
98
|
+
|
99
|
+
def valid_detector?(detector)
|
100
|
+
descendants.map { |descendant| descendant.to_s.split('::').last }
|
101
|
+
.include?(detector)
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_detector(detector_name)
|
105
|
+
SmellDetectors.const_get detector_name
|
106
|
+
end
|
107
|
+
|
108
|
+
def configuration_keys
|
109
|
+
Set.new(default_config.keys.map(&:to_sym))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class LongParameterList < BaseDetector
|
8
|
+
MAX_ALLOWED_PARAMS_KEY = 'max_params'
|
9
|
+
|
10
|
+
def self.default_config
|
11
|
+
super.merge(
|
12
|
+
MAX_ALLOWED_PARAMS_KEY => 3,
|
13
|
+
SmellConfiguration::OVERRIDES_KEY => {
|
14
|
+
'initialize' => { MAX_ALLOWED_PARAMS_KEY => 3 }
|
15
|
+
}
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def sniff
|
20
|
+
count = expression.arg_names.length
|
21
|
+
|
22
|
+
return [] if count <= max_allowed_params
|
23
|
+
|
24
|
+
message = "has #{count} parameters. "\
|
25
|
+
"We propose to use Builder Pattern."
|
26
|
+
|
27
|
+
[
|
28
|
+
smell_warning(
|
29
|
+
context: context,
|
30
|
+
lines: [source_line],
|
31
|
+
message: message,
|
32
|
+
parameters: { count: count }
|
33
|
+
)
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def max_allowed_params
|
40
|
+
value(MAX_ALLOWED_PARAMS_KEY, context)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class TooManyConstants < BaseDetector
|
8
|
+
MAX_ALLOWED_CONSTANTS_KEY = 'max_constants'
|
9
|
+
DEFAULT_MAX_CONSTANTS = 3
|
10
|
+
IGNORED_NODES = %i[module class].freeze
|
11
|
+
|
12
|
+
def self.contexts
|
13
|
+
%i[class module]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.default_config
|
17
|
+
super.merge(
|
18
|
+
MAX_ALLOWED_CONSTANTS_KEY => DEFAULT_MAX_CONSTANTS,
|
19
|
+
EXCLUDE_KEY => []
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sniff
|
24
|
+
count = constants_count
|
25
|
+
|
26
|
+
return [] if count <= max_allowed_constants
|
27
|
+
|
28
|
+
build_smell_warning(count)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def constants_count
|
34
|
+
context.each_node(:casgn, IGNORED_NODES)
|
35
|
+
.delete_if(&:defines_module?).length
|
36
|
+
end
|
37
|
+
|
38
|
+
def max_allowed_constants
|
39
|
+
value(MAX_ALLOWED_CONSTANTS_KEY, context)
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_smell_warning(count)
|
43
|
+
[
|
44
|
+
smell_warning(
|
45
|
+
context: context,
|
46
|
+
lines: [source_line],
|
47
|
+
message: "has #{count} constants",
|
48
|
+
parameters: { count: count }
|
49
|
+
)
|
50
|
+
]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class TooManyInstanceVariables < BaseDetector
|
8
|
+
MAX_ALLOWED_IVARS_KEY = 'max_instance_variables'
|
9
|
+
DEFAULT_MAX_IVARS = 3
|
10
|
+
|
11
|
+
def self.contexts
|
12
|
+
[:class]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default_config
|
16
|
+
super.merge(
|
17
|
+
MAX_ALLOWED_IVARS_KEY => DEFAULT_MAX_IVARS,
|
18
|
+
EXCLUDE_KEY => []
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sniff
|
23
|
+
variables = context.local_nodes(:ivasgn, [:or_asgn]).map(&:name)
|
24
|
+
count = variables.uniq.size
|
25
|
+
|
26
|
+
return [] if count <= max_allowed_ivars
|
27
|
+
|
28
|
+
message = "has at least #{count} instance variables. "\
|
29
|
+
"We propose to use Facade Pattern"
|
30
|
+
|
31
|
+
[
|
32
|
+
smell_warning(
|
33
|
+
context: context,
|
34
|
+
lines: [source_line],
|
35
|
+
message: message,
|
36
|
+
parameters: { count: count }
|
37
|
+
)
|
38
|
+
]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def max_allowed_ivars
|
44
|
+
value(MAX_ALLOWED_IVARS_KEY, context)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class TooManyMethods < BaseDetector
|
8
|
+
MAX_ALLOWED_METHODS_KEY = 'max_methods'
|
9
|
+
DEFAULT_MAX_METHODS = 5
|
10
|
+
|
11
|
+
def self.contexts
|
12
|
+
[:class]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default_config
|
16
|
+
super.merge(
|
17
|
+
MAX_ALLOWED_METHODS_KEY => DEFAULT_MAX_METHODS,
|
18
|
+
EXCLUDE_KEY => []
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sniff
|
23
|
+
count = context.node_instance_methods.length
|
24
|
+
|
25
|
+
return [] if count <= max_allowed_methods
|
26
|
+
|
27
|
+
message = "Your class has #{count} methods. "\
|
28
|
+
'We propose to use ExtractClass Pattern'
|
29
|
+
|
30
|
+
[
|
31
|
+
smell_warning(
|
32
|
+
context: context,
|
33
|
+
lines: [source_line],
|
34
|
+
message: message,
|
35
|
+
parameters: { count: count }
|
36
|
+
)
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def max_allowed_methods
|
43
|
+
value(MAX_ALLOWED_METHODS_KEY, context)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class TooManyStatements < BaseDetector
|
8
|
+
MAX_ALLOWED_STATEMENTS_KEY = 'max_statements'
|
9
|
+
DEFAULT_MAX_STATEMENTS = 5
|
10
|
+
|
11
|
+
def self.default_config
|
12
|
+
super.merge(
|
13
|
+
MAX_ALLOWED_STATEMENTS_KEY => DEFAULT_MAX_STATEMENTS,
|
14
|
+
EXCLUDE_KEY => ['initialize']
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sniff
|
19
|
+
count = context.number_of_statements
|
20
|
+
|
21
|
+
return [] if count <= max_allowed_statements
|
22
|
+
|
23
|
+
message = "Your method has #{count} statements. "\
|
24
|
+
'We propose to use ExtractMethod Pattern'
|
25
|
+
|
26
|
+
[
|
27
|
+
smell_warning(
|
28
|
+
context: context,
|
29
|
+
lines: [source_line],
|
30
|
+
message: message,
|
31
|
+
parameters: { count: count }
|
32
|
+
)
|
33
|
+
]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def max_allowed_statements
|
39
|
+
value(MAX_ALLOWED_STATEMENTS_KEY, context)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class UncommunicativeMethodName < BaseDetector
|
8
|
+
REJECT_KEY = 'reject'
|
9
|
+
ACCEPT_KEY = 'accept'
|
10
|
+
DEFAULT_REJECT_PATTERNS = [/^[a-z]$/, /[0-9]$/, /[A-Z]/].freeze
|
11
|
+
DEFAULT_ACCEPT_PATTERNS = [].freeze
|
12
|
+
|
13
|
+
def self.default_config
|
14
|
+
super.merge(
|
15
|
+
REJECT_KEY => DEFAULT_REJECT_PATTERNS,
|
16
|
+
ACCEPT_KEY => DEFAULT_ACCEPT_PATTERNS
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def sniff
|
21
|
+
name = context.name.to_s
|
22
|
+
|
23
|
+
return [] if acceptable_name?(name)
|
24
|
+
|
25
|
+
[
|
26
|
+
smell_warning(
|
27
|
+
context: context,
|
28
|
+
lines: [source_line],
|
29
|
+
message: "has the name '#{name}'",
|
30
|
+
parameters: { name: name }
|
31
|
+
)
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def acceptable_name?(name)
|
38
|
+
any_acceptance_patterns?(name) || any_reject_patterns?(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def any_acceptance_patterns?(name)
|
42
|
+
accept_patterns.any? { |accept_pattern| name.match accept_pattern }
|
43
|
+
end
|
44
|
+
|
45
|
+
def any_reject_patterns?(name)
|
46
|
+
reject_patterns.none? { |reject_pattern| name.match reject_pattern }
|
47
|
+
end
|
48
|
+
|
49
|
+
def reject_patterns
|
50
|
+
Array value(REJECT_KEY, context)
|
51
|
+
end
|
52
|
+
|
53
|
+
def accept_patterns
|
54
|
+
Array value(ACCEPT_KEY, context)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class UncommunicativeModuleName < BaseDetector
|
8
|
+
REJECT_KEY = 'reject'
|
9
|
+
DEFAULT_REJECT_PATTERNS = [/^.$/, /[0-9]$/].freeze
|
10
|
+
ACCEPT_KEY = 'accept'
|
11
|
+
DEFAULT_ACCEPT_PATTERNS = [].freeze
|
12
|
+
|
13
|
+
def self.default_config
|
14
|
+
super.merge(
|
15
|
+
REJECT_KEY => DEFAULT_REJECT_PATTERNS,
|
16
|
+
ACCEPT_KEY => DEFAULT_ACCEPT_PATTERNS
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.contexts
|
21
|
+
%i[module class]
|
22
|
+
end
|
23
|
+
|
24
|
+
def sniff
|
25
|
+
fully_qualified_name = context.full_name
|
26
|
+
module_name = expression.simple_name
|
27
|
+
|
28
|
+
return [] if acceptable_name?(
|
29
|
+
module_name: module_name,
|
30
|
+
fully_qualified_name: fully_qualified_name
|
31
|
+
)
|
32
|
+
|
33
|
+
[
|
34
|
+
smell_warning(
|
35
|
+
context: context,
|
36
|
+
lines: [source_line],
|
37
|
+
message: "has the name '#{module_name}'",
|
38
|
+
parameters: { name: module_name }
|
39
|
+
)
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def acceptable_name?(module_name:, fully_qualified_name:)
|
46
|
+
any_accept_patterns?(module_name) ||
|
47
|
+
any_reject_patterns?(fully_qualified_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def any_accept_patterns?(fully_qualified_name)
|
51
|
+
accept_patterns.any? do |accept_pattern|
|
52
|
+
fully_qualified_name.match accept_pattern
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def any_reject_patterns?(module_name)
|
57
|
+
reject_patterns.none? do |reject_pattern|
|
58
|
+
module_name.match reject_pattern
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def reject_patterns
|
63
|
+
@reject_patterns ||= Array value(REJECT_KEY, context)
|
64
|
+
end
|
65
|
+
|
66
|
+
def accept_patterns
|
67
|
+
@accept_patterns ||= Array value(ACCEPT_KEY, context)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|