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,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'context/attribute_context'
|
4
|
+
require_relative 'context/method_context'
|
5
|
+
require_relative 'context/module_context'
|
6
|
+
require_relative 'context/root_context'
|
7
|
+
require_relative 'context/send_context'
|
8
|
+
require_relative 'context/singleton_attribute_context'
|
9
|
+
require_relative 'context/singleton_method_context'
|
10
|
+
require_relative 'ast/node'
|
11
|
+
|
12
|
+
module ImproveYourCode
|
13
|
+
class ContextBuilder
|
14
|
+
attr_reader :context_tree
|
15
|
+
|
16
|
+
def initialize(syntax_tree)
|
17
|
+
@exp = syntax_tree
|
18
|
+
@current_context = Context::RootContext.new(exp)
|
19
|
+
@context_tree = build(exp)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_accessor :current_context
|
25
|
+
attr_reader :exp
|
26
|
+
|
27
|
+
def build(exp, parent_exp = nil)
|
28
|
+
context_processor = "process_#{exp.type}"
|
29
|
+
|
30
|
+
if context_processor_exists?(context_processor)
|
31
|
+
send(context_processor, exp, parent_exp)
|
32
|
+
else
|
33
|
+
process exp
|
34
|
+
end
|
35
|
+
current_context
|
36
|
+
end
|
37
|
+
|
38
|
+
def process(exp)
|
39
|
+
exp.children.grep(AST::Node).each { |child| build(child, exp) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_module(exp, _parent)
|
43
|
+
inside_new_context(Context::ModuleContext, exp) do
|
44
|
+
process(exp)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
alias process_class process_module
|
49
|
+
|
50
|
+
def process_def(exp, parent)
|
51
|
+
inside_new_context(current_context.method_context_class, exp, parent) do
|
52
|
+
increase_statement_count_by(exp.body)
|
53
|
+
process(exp)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_send(exp, _parent)
|
58
|
+
process(exp)
|
59
|
+
case current_context
|
60
|
+
when Context::ModuleContext
|
61
|
+
handle_send_for_modules exp
|
62
|
+
when Context::MethodContext
|
63
|
+
handle_send_for_methods exp
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_begin(exp, _parent)
|
68
|
+
increase_statement_count_by(exp.children)
|
69
|
+
decrease_statement_count
|
70
|
+
process(exp)
|
71
|
+
end
|
72
|
+
|
73
|
+
alias process_kwbegin process_begin
|
74
|
+
|
75
|
+
def context_processor_exists?(name)
|
76
|
+
self.class.private_method_defined?(name)
|
77
|
+
end
|
78
|
+
|
79
|
+
def increase_statement_count_by(sexp)
|
80
|
+
current_context.statement_counter.increase_by sexp
|
81
|
+
end
|
82
|
+
|
83
|
+
def decrease_statement_count
|
84
|
+
current_context.statement_counter.decrease_by 1
|
85
|
+
end
|
86
|
+
|
87
|
+
def inside_new_context(klass, *args)
|
88
|
+
new_context = append_new_context(klass, *args)
|
89
|
+
|
90
|
+
orig = current_context
|
91
|
+
self.current_context = new_context
|
92
|
+
yield
|
93
|
+
self.current_context = orig
|
94
|
+
end
|
95
|
+
|
96
|
+
def append_new_context(klass, *args)
|
97
|
+
klass.new(*args).tap do |new_context|
|
98
|
+
new_context.register_with_parent(current_context)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_send_for_modules(exp)
|
103
|
+
arg_names = exp.args.map { |arg| arg.children.first }
|
104
|
+
current_context.track_visibility(exp.name, arg_names)
|
105
|
+
register_attributes(exp)
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_send_for_methods(exp)
|
109
|
+
append_new_context(Context::SendContext, exp, exp.name)
|
110
|
+
current_context.record_call_to(exp)
|
111
|
+
end
|
112
|
+
|
113
|
+
def register_attributes(exp)
|
114
|
+
return unless exp.attribute_writer?
|
115
|
+
klass = current_context.attribute_context_class
|
116
|
+
exp.args.each do |arg|
|
117
|
+
append_new_context(klass, arg, exp)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'smell_detectors'
|
4
|
+
require_relative 'smell_detectors/base_detector'
|
5
|
+
|
6
|
+
module ImproveYourCode
|
7
|
+
class DetectorRepository
|
8
|
+
def self.eligible_smell_types
|
9
|
+
ImproveYourCode::SmellDetectors::BaseDetector
|
10
|
+
.descendants.sort_by(&:name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(smell_types: self.class.smell_types)
|
14
|
+
@smell_types = smell_types
|
15
|
+
end
|
16
|
+
|
17
|
+
def examine(context)
|
18
|
+
smell_detectors_for(context.type).flat_map do |klass|
|
19
|
+
detector = klass.new context: context
|
20
|
+
detector.run
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :smell_types
|
27
|
+
|
28
|
+
def smell_detectors_for(type)
|
29
|
+
smell_types.select { |detector| detector.contexts.include? type }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'context_builder'
|
4
|
+
require_relative 'detector_repository'
|
5
|
+
require_relative 'source/source_code'
|
6
|
+
|
7
|
+
module ImproveYourCode
|
8
|
+
class Examiner
|
9
|
+
def initialize(source)
|
10
|
+
@source = Source::SourceCode.from(source)
|
11
|
+
@smell_types = DetectorRepository.eligible_smell_types
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
@description ||= source.origin
|
16
|
+
end
|
17
|
+
|
18
|
+
def smells
|
19
|
+
@smells ||= examine_tree.sort.uniq
|
20
|
+
end
|
21
|
+
|
22
|
+
def smells_count
|
23
|
+
smells.length
|
24
|
+
end
|
25
|
+
|
26
|
+
def smelly?
|
27
|
+
smells.any?
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :source
|
33
|
+
|
34
|
+
def detector_repository
|
35
|
+
DetectorRepository.new(smell_types: @smell_types)
|
36
|
+
end
|
37
|
+
|
38
|
+
def syntax_tree
|
39
|
+
@syntax_tree ||= source.syntax_tree
|
40
|
+
end
|
41
|
+
|
42
|
+
def examine_tree
|
43
|
+
ContextBuilder.new(syntax_tree).context_tree.flat_map do |element|
|
44
|
+
detector_repository.examine(element)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'formatter/heading_formatter'
|
4
|
+
require_relative 'formatter/progress_formatter'
|
5
|
+
require_relative 'formatter/simple_warning_formatter'
|
6
|
+
|
7
|
+
module ImproveYourCode
|
8
|
+
module Report
|
9
|
+
module Formatter
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def format_list(warnings, formatter: SimpleWarningFormatter.new)
|
13
|
+
warnings.map { |warning| " #{formatter.format(warning)}" }.join("\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
def header(examiner)
|
17
|
+
count = examiner.smells_count
|
18
|
+
result = Rainbow("#{examiner.description} -- ").cyan +
|
19
|
+
Rainbow("#{count} warning").yellow
|
20
|
+
result += Rainbow('s').yellow unless count == 1
|
21
|
+
result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImproveYourCode
|
4
|
+
module Report
|
5
|
+
module Formatter
|
6
|
+
class HeadingFormatterBase
|
7
|
+
attr_reader :report_formatter
|
8
|
+
|
9
|
+
def initialize(report_formatter)
|
10
|
+
@report_formatter = report_formatter
|
11
|
+
end
|
12
|
+
|
13
|
+
def show_header?(_examiner)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def header(examiner)
|
18
|
+
if show_header?(examiner)
|
19
|
+
report_formatter.header examiner
|
20
|
+
else
|
21
|
+
''
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class QuietHeadingFormatter < HeadingFormatterBase
|
27
|
+
def show_header?(examiner)
|
28
|
+
examiner.smelly?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImproveYourCode
|
4
|
+
module Report
|
5
|
+
module Formatter
|
6
|
+
module ProgressFormatter
|
7
|
+
class Dots
|
8
|
+
def progress(examiner)
|
9
|
+
examiner.smelly? ? display_smelly : display_clean
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def display_clean
|
15
|
+
Rainbow('.').color(:green)
|
16
|
+
end
|
17
|
+
|
18
|
+
def display_smelly
|
19
|
+
Rainbow('W').color(:red)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'pathname'
|
5
|
+
require 'rainbow'
|
6
|
+
|
7
|
+
require_relative 'formatter'
|
8
|
+
|
9
|
+
module ImproveYourCode
|
10
|
+
module Report
|
11
|
+
class TextReport
|
12
|
+
NO_WARNINGS_COLOR = :green
|
13
|
+
WARNINGS_COLOR = :red
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@examiners = []
|
17
|
+
@total_smell_count = 0
|
18
|
+
@heading_formatter = Formatter::QuietHeadingFormatter.new(Formatter)
|
19
|
+
@progress_formatter = Report::Formatter::ProgressFormatter::Dots.new
|
20
|
+
@warning_formatter = Report::Formatter::SimpleWarningFormatter.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_examiner(examiner)
|
24
|
+
print progress_formatter.progress examiner
|
25
|
+
|
26
|
+
self.total_smell_count += examiner.smells_count
|
27
|
+
|
28
|
+
examiners << examiner
|
29
|
+
end
|
30
|
+
|
31
|
+
def show
|
32
|
+
print "\n\n"
|
33
|
+
|
34
|
+
display_summary
|
35
|
+
|
36
|
+
print total_smell_count_message
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
attr_accessor :total_smell_count
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :examiners, :heading_formatter,
|
46
|
+
:warning_formatter, :progress_formatter
|
47
|
+
|
48
|
+
def smell_summaries
|
49
|
+
examiners.map { |ex| summarize_single_examiner(ex) }.reject(&:empty?)
|
50
|
+
end
|
51
|
+
|
52
|
+
def display_summary
|
53
|
+
smell_summaries.each { |smell| puts smell }
|
54
|
+
end
|
55
|
+
|
56
|
+
def summarize_single_examiner(examiner)
|
57
|
+
result = heading_formatter.header(examiner)
|
58
|
+
if examiner.smelly?
|
59
|
+
formatted_list = Formatter.format_list(examiner.smells,
|
60
|
+
formatter: warning_formatter)
|
61
|
+
result += ":\n#{formatted_list}"
|
62
|
+
end
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def smells?
|
67
|
+
total_smell_count > 0
|
68
|
+
end
|
69
|
+
|
70
|
+
def total_smell_count_message
|
71
|
+
colour = smells? ? WARNINGS_COLOR : NO_WARNINGS_COLOR
|
72
|
+
Rainbow("#{total_smell_count} total warning#{total_smell_count == 1 ? '' : 's'}\n").color(colour)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImproveYourCode
|
4
|
+
class SmellConfiguration
|
5
|
+
ENABLED_KEY = 'enabled'
|
6
|
+
OVERRIDES_KEY = 'overrides'
|
7
|
+
|
8
|
+
def initialize(hash)
|
9
|
+
@options = hash
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge(new_options)
|
13
|
+
options.merge!(new_options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def enabled?
|
17
|
+
options[ENABLED_KEY]
|
18
|
+
end
|
19
|
+
|
20
|
+
def overrides_for(context)
|
21
|
+
Overrides.new(options.fetch(OVERRIDES_KEY, {})).for_context(context)
|
22
|
+
end
|
23
|
+
|
24
|
+
def value(key, context)
|
25
|
+
overrides_for(context).each { |conf| return conf[key] if conf.key?(key) }
|
26
|
+
options.fetch(key)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :options
|
32
|
+
end
|
33
|
+
|
34
|
+
class Overrides
|
35
|
+
def initialize(hash)
|
36
|
+
@hash = hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def for_context(context)
|
40
|
+
contexts = hash.keys.select { |ckey| context.matches?([ckey]) }
|
41
|
+
contexts.map { |exc| hash[exc] }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :hash
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'smell_detectors/long_parameter_list'
|
4
|
+
require_relative 'smell_detectors/too_many_instance_variables'
|
5
|
+
require_relative 'smell_detectors/too_many_constants'
|
6
|
+
require_relative 'smell_detectors/too_many_methods'
|
7
|
+
require_relative 'smell_detectors/too_many_statements'
|
8
|
+
require_relative 'smell_detectors/uncommunicative_method_name'
|
9
|
+
require_relative 'smell_detectors/uncommunicative_module_name'
|
10
|
+
require_relative 'smell_detectors/uncommunicative_variable_name'
|
11
|
+
require_relative 'smell_detectors/unused_parameters'
|
12
|
+
require_relative 'smell_detectors/unused_private_method'
|