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,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class UncommunicativeVariableName < BaseDetector
|
8
|
+
REJECT_KEY = 'reject'
|
9
|
+
DEFAULT_REJECT_SET = [
|
10
|
+
/^.$/, # single-character names
|
11
|
+
/[0-9]$/, # any name ending with a number
|
12
|
+
/[A-Z]/ # camelCaseVariableNames
|
13
|
+
].freeze
|
14
|
+
ACCEPT_KEY = 'accept'
|
15
|
+
DEFAULT_ACCEPT_SET = [/^_$/].freeze
|
16
|
+
|
17
|
+
def self.default_config
|
18
|
+
super.merge(
|
19
|
+
REJECT_KEY => DEFAULT_REJECT_SET,
|
20
|
+
ACCEPT_KEY => DEFAULT_ACCEPT_SET
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.contexts
|
25
|
+
%i[module class def defs]
|
26
|
+
end
|
27
|
+
|
28
|
+
def sniff
|
29
|
+
variable_names.select do |name, _lines|
|
30
|
+
uncommunicative_variable_name?(name)
|
31
|
+
end.map do |name, lines|
|
32
|
+
smell_warning(
|
33
|
+
context: context,
|
34
|
+
lines: lines,
|
35
|
+
message: "has the variable name '#{name}'",
|
36
|
+
parameters: { name: name.to_s }
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def reject_names
|
44
|
+
@reject_names ||= value(REJECT_KEY, context)
|
45
|
+
end
|
46
|
+
|
47
|
+
def accept_names
|
48
|
+
@accept_names ||= value(ACCEPT_KEY, context)
|
49
|
+
end
|
50
|
+
|
51
|
+
def uncommunicative_variable_name?(name)
|
52
|
+
sanitized_name = name.to_s.gsub(/^[@\*\&]*/, '')
|
53
|
+
!acceptable_name?(sanitized_name)
|
54
|
+
end
|
55
|
+
|
56
|
+
def acceptable_name?(name)
|
57
|
+
any_accept_names?(name) || any_reject_names?(name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def any_accept_names?(name)
|
61
|
+
Array(accept_names).any? { |accept_pattern| name.match accept_pattern }
|
62
|
+
end
|
63
|
+
|
64
|
+
def any_reject_names?(name)
|
65
|
+
Array(reject_names).none? { |reject_pattern| name.match reject_pattern }
|
66
|
+
end
|
67
|
+
|
68
|
+
def variable_names
|
69
|
+
result = Hash.new { |hash, key| hash[key] = [] }
|
70
|
+
|
71
|
+
find_assignment_variable_names(result)
|
72
|
+
find_block_argument_variable_names(result)
|
73
|
+
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_assignment_variable_names(accumulator)
|
78
|
+
assignment_nodes = expression.each_node(
|
79
|
+
:lvasgn, %i[class module defs def]
|
80
|
+
)
|
81
|
+
|
82
|
+
case expression.type
|
83
|
+
when :class, :module
|
84
|
+
assignment_nodes += expression.each_node(:ivasgn, %i[class module])
|
85
|
+
end
|
86
|
+
|
87
|
+
assignment_nodes.each do |asgn|
|
88
|
+
accumulator[asgn.children.first].push(asgn.line)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_block_argument_variable_names(accumulator)
|
93
|
+
arg_search_exp = case expression.type
|
94
|
+
when :class, :module
|
95
|
+
expression
|
96
|
+
when :defs, :def
|
97
|
+
expression.body
|
98
|
+
end
|
99
|
+
|
100
|
+
return unless arg_search_exp
|
101
|
+
|
102
|
+
args_nodes = arg_search_exp
|
103
|
+
.each_node(:args, %i[class module defs def])
|
104
|
+
|
105
|
+
args_nodes.each do |args_node|
|
106
|
+
recursively_record_variable_names(accumulator, args_node)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def recursively_record_variable_names(accumulator, exp)
|
111
|
+
exp.children.each do |subexp|
|
112
|
+
case subexp.type
|
113
|
+
when :mlhs
|
114
|
+
recursively_record_variable_names(accumulator, subexp)
|
115
|
+
else
|
116
|
+
record_variable_name(exp, subexp.name, accumulator)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def record_variable_name(exp, symbol, accumulator)
|
122
|
+
varname = symbol.to_s.sub(/^\*/, '')
|
123
|
+
return if varname == ''
|
124
|
+
var = varname.to_sym
|
125
|
+
accumulator[var].push(exp.line)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class UnusedParameters < BaseDetector
|
8
|
+
def sniff
|
9
|
+
return [] if context.uses_super_with_implicit_arguments?
|
10
|
+
|
11
|
+
context.unused_params.map do |param|
|
12
|
+
name = param.name.to_s
|
13
|
+
|
14
|
+
smell_warning(
|
15
|
+
context: context,
|
16
|
+
lines: [source_line],
|
17
|
+
message: "has unused parameter '#{name}'",
|
18
|
+
parameters: { name: name }
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_detector'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
module SmellDetectors
|
7
|
+
class UnusedPrivateMethod < BaseDetector
|
8
|
+
def self.default_config
|
9
|
+
super.merge(SmellConfiguration::ENABLED_KEY => false)
|
10
|
+
end
|
11
|
+
|
12
|
+
class Hit
|
13
|
+
attr_reader :name, :line
|
14
|
+
|
15
|
+
def initialize(context)
|
16
|
+
@name = context.name
|
17
|
+
@line = context.exp.line
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.contexts
|
22
|
+
[:class]
|
23
|
+
end
|
24
|
+
|
25
|
+
def sniff
|
26
|
+
hits.map do |hit|
|
27
|
+
name = hit.name
|
28
|
+
|
29
|
+
smell_warning(
|
30
|
+
context: context,
|
31
|
+
lines: [hit.line],
|
32
|
+
message: "has the unused private instance method '#{name}'",
|
33
|
+
parameters: { name: name.to_s }
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def hits
|
41
|
+
unused_private_methods.map do |defined_method|
|
42
|
+
Hit.new(defined_method) unless ignore_method?(defined_method)
|
43
|
+
end.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def unused_private_methods
|
47
|
+
defined_private_methods.reject do |defined_method|
|
48
|
+
called_method_names.include?(defined_method.name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def defined_private_methods
|
53
|
+
context.defined_instance_methods(visibility: :private)
|
54
|
+
end
|
55
|
+
|
56
|
+
def called_method_names
|
57
|
+
context.instance_method_calls.map(&:name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def ignore_method?(method)
|
61
|
+
ignore_contexts = value(EXCLUDE_KEY, context)
|
62
|
+
ignore_contexts.any? do |ignore_context|
|
63
|
+
full_name = "#{method.parent.full_name}##{method.name}"
|
64
|
+
full_name[ignore_context]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
class SmellWarning
|
7
|
+
include Comparable
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :context, :lines, :message, :parameters, :smell_detector, :source
|
11
|
+
def_delegators :smell_detector, :smell_type
|
12
|
+
|
13
|
+
def initialize(smell_detector, context: '', lines:, message:,
|
14
|
+
source:, parameters: {})
|
15
|
+
@smell_detector = smell_detector
|
16
|
+
@source = source
|
17
|
+
@context = context.to_s
|
18
|
+
@lines = lines
|
19
|
+
@message = message
|
20
|
+
@parameters = parameters
|
21
|
+
|
22
|
+
freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def hash
|
26
|
+
identifying_values.hash
|
27
|
+
end
|
28
|
+
|
29
|
+
def <=>(other)
|
30
|
+
identifying_values <=> other.identifying_values
|
31
|
+
end
|
32
|
+
|
33
|
+
def eql?(other)
|
34
|
+
(self <=> other).zero?
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_hash
|
38
|
+
stringified_params = Hash[parameters.map { |key, val| [key.to_s, val] }]
|
39
|
+
base_hash.merge(stringified_params)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias yaml_hash to_hash
|
43
|
+
|
44
|
+
def base_message
|
45
|
+
"#{smell_type}: #{context} #{message}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def smell_class
|
49
|
+
smell_detector.class
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def identifying_values
|
55
|
+
[smell_type, context, message, lines]
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def base_hash
|
61
|
+
{
|
62
|
+
'context' => context,
|
63
|
+
'lines' => lines,
|
64
|
+
'message' => message,
|
65
|
+
'smell_type' => smell_type,
|
66
|
+
'source' => source
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../cli/silencer'
|
4
|
+
ImproveYourCode::CLI::Silencer.silently do
|
5
|
+
require 'parser/ruby24'
|
6
|
+
end
|
7
|
+
require_relative '../tree_dresser'
|
8
|
+
require_relative '../ast/node'
|
9
|
+
require_relative '../ast/builder'
|
10
|
+
|
11
|
+
ImproveYourCode::AST::Builder.emit_lambda = true
|
12
|
+
|
13
|
+
module ImproveYourCode
|
14
|
+
module Source
|
15
|
+
class SourceCode
|
16
|
+
IO_IDENTIFIER = 'STDIN'
|
17
|
+
STRING_IDENTIFIER = 'string'
|
18
|
+
|
19
|
+
attr_reader :origin
|
20
|
+
|
21
|
+
def initialize(code:, origin:, parser: default_parser)
|
22
|
+
@origin = origin
|
23
|
+
@diagnostics = []
|
24
|
+
@parser = parser
|
25
|
+
@code = code
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.from(source)
|
29
|
+
case source
|
30
|
+
when File then new(code: source.read, origin: source.path)
|
31
|
+
when IO then new(code: source.readlines.join, origin: IO_IDENTIFIER)
|
32
|
+
when Pathname then new(code: source.read, origin: source.to_s)
|
33
|
+
when String then new(code: source, origin: STRING_IDENTIFIER)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_syntax?
|
38
|
+
diagnostics.none? do |diagnostic|
|
39
|
+
%i[error fatal].include?(diagnostic.level)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def diagnostics
|
44
|
+
parse_if_needed
|
45
|
+
|
46
|
+
@diagnostics
|
47
|
+
end
|
48
|
+
|
49
|
+
def syntax_tree
|
50
|
+
parse_if_needed
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def parse_if_needed
|
56
|
+
@syntax_tree ||= parse(@parser, @code)
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :source
|
60
|
+
|
61
|
+
def parse(parser, source)
|
62
|
+
buffer = Parser::Source::Buffer.new(origin, 1)
|
63
|
+
source.force_encoding(Encoding::UTF_8)
|
64
|
+
buffer.source = source
|
65
|
+
ast, comments = parser.parse_with_comments(buffer)
|
66
|
+
|
67
|
+
comment_map = Parser::Source::Comment.associate(ast, comments) if ast
|
68
|
+
|
69
|
+
TreeDresser.new.dress(ast, comment_map)
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_parser
|
73
|
+
Parser::Ruby24.new(AST::Builder.new).tap do |parser|
|
74
|
+
diagnostics = parser.diagnostics
|
75
|
+
diagnostics.all_errors_are_fatal = false
|
76
|
+
diagnostics.ignore_warnings = false
|
77
|
+
diagnostics.consumer = lambda do |diagnostic|
|
78
|
+
@diagnostics << diagnostic
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'find'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module ImproveYourCode
|
7
|
+
module Source
|
8
|
+
class SourceLocator
|
9
|
+
def initialize(paths)
|
10
|
+
@paths = paths.flat_map { |string| Pathname.new(string).entries }
|
11
|
+
end
|
12
|
+
|
13
|
+
def sources
|
14
|
+
paths.each_with_object([]) do |given_path, relevant_paths|
|
15
|
+
given_path.find do |path|
|
16
|
+
if path.directory?
|
17
|
+
ignore_path?(path) ? Find.prune : next
|
18
|
+
elsif ruby_file?(path)
|
19
|
+
relevant_paths << path
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :paths
|
28
|
+
|
29
|
+
def ignore_path?(path)
|
30
|
+
path.basename.to_s.start_with? '.'
|
31
|
+
end
|
32
|
+
|
33
|
+
def ruby_file?(path)
|
34
|
+
path.extname == '.rb'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'ast/ast_node_class_map'
|
4
|
+
|
5
|
+
module ImproveYourCode
|
6
|
+
class TreeDresser
|
7
|
+
def initialize(klass_map: AST::ASTNodeClassMap.new)
|
8
|
+
@klass_map = klass_map
|
9
|
+
end
|
10
|
+
|
11
|
+
def dress(sexp, comment_map)
|
12
|
+
return sexp unless sexp.is_a? ::Parser::AST::Node
|
13
|
+
|
14
|
+
type = sexp.type
|
15
|
+
children = sexp.children.map { |child| dress(child, comment_map) }
|
16
|
+
comments = comment_map[sexp]
|
17
|
+
|
18
|
+
klass_map.klass_for(type).new(type, children, location: sexp.loc, comments: comments)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :klass_map
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
['const',
|
29
|
+
'class',
|
30
|
+
'module',
|
31
|
+
'args',
|
32
|
+
'cbase',
|
33
|
+
'sym',
|
34
|
+
'send',
|
35
|
+
'pair',
|
36
|
+
'hash',
|
37
|
+
'lvasgn',
|
38
|
+
'lvar',
|
39
|
+
'begin',
|
40
|
+
'def',
|
41
|
+
'str',
|
42
|
+
'optarg',
|
43
|
+
'arg',
|
44
|
+
'return',
|
45
|
+
'and',
|
46
|
+
'if',
|
47
|
+
'array',
|
48
|
+
'casgn',
|
49
|
+
'block',
|
50
|
+
'int',
|
51
|
+
'self',
|
52
|
+
'true',
|
53
|
+
'false',
|
54
|
+
'dstr',
|
55
|
+
'block_pass',
|
56
|
+
'kwarg',
|
57
|
+
'resbody',
|
58
|
+
'rescue',
|
59
|
+
'sclass',
|
60
|
+
'nil',
|
61
|
+
'ivasgn',
|
62
|
+
'or_asgn',
|
63
|
+
'regopt',
|
64
|
+
'regexp',
|
65
|
+
'csend',
|
66
|
+
'kwbegin',
|
67
|
+
'or',
|
68
|
+
'kwoptarg',
|
69
|
+
'splat',
|
70
|
+
'float',
|
71
|
+
'op_asgn',
|
72
|
+
'kwsplat',
|
73
|
+
'defs'
|
74
|
+
]
|