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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +12 -0
  4. data/README.md +0 -0
  5. data/Rakefile +4 -0
  6. data/bin/improve_your_code +6 -0
  7. data/improve_your_code.gemspec +22 -0
  8. data/lib/improve_your_code/ast/ast_node_class_map.rb +39 -0
  9. data/lib/improve_your_code/ast/builder.rb +11 -0
  10. data/lib/improve_your_code/ast/node.rb +108 -0
  11. data/lib/improve_your_code/ast/object_refs.rb +32 -0
  12. data/lib/improve_your_code/ast/reference_collector.rb +29 -0
  13. data/lib/improve_your_code/ast/sexp_extensions.rb +15 -0
  14. data/lib/improve_your_code/ast/sexp_extensions/arguments.rb +89 -0
  15. data/lib/improve_your_code/ast/sexp_extensions/block.rb +38 -0
  16. data/lib/improve_your_code/ast/sexp_extensions/constant.rb +13 -0
  17. data/lib/improve_your_code/ast/sexp_extensions/if.rb +18 -0
  18. data/lib/improve_your_code/ast/sexp_extensions/methods.rb +81 -0
  19. data/lib/improve_your_code/ast/sexp_extensions/module.rb +64 -0
  20. data/lib/improve_your_code/ast/sexp_extensions/nested_assignables.rb +17 -0
  21. data/lib/improve_your_code/ast/sexp_extensions/self.rb +13 -0
  22. data/lib/improve_your_code/ast/sexp_extensions/send.rb +55 -0
  23. data/lib/improve_your_code/ast/sexp_extensions/symbols.rb +14 -0
  24. data/lib/improve_your_code/ast/sexp_extensions/variables.rb +45 -0
  25. data/lib/improve_your_code/cli/application.rb +32 -0
  26. data/lib/improve_your_code/cli/command/report_command.rb +39 -0
  27. data/lib/improve_your_code/cli/silencer.rb +24 -0
  28. data/lib/improve_your_code/code_comment.rb +52 -0
  29. data/lib/improve_your_code/context/attribute_context.rb +33 -0
  30. data/lib/improve_your_code/context/code_context.rb +121 -0
  31. data/lib/improve_your_code/context/method_context.rb +79 -0
  32. data/lib/improve_your_code/context/module_context.rb +77 -0
  33. data/lib/improve_your_code/context/root_context.rb +22 -0
  34. data/lib/improve_your_code/context/send_context.rb +16 -0
  35. data/lib/improve_your_code/context/singleton_attribute_context.rb +13 -0
  36. data/lib/improve_your_code/context/singleton_method_context.rb +29 -0
  37. data/lib/improve_your_code/context/statement_counter.rb +27 -0
  38. data/lib/improve_your_code/context/visibility_tracker.rb +52 -0
  39. data/lib/improve_your_code/context_builder.rb +121 -0
  40. data/lib/improve_your_code/detector_repository.rb +32 -0
  41. data/lib/improve_your_code/examiner.rb +48 -0
  42. data/lib/improve_your_code/report/formatter.rb +25 -0
  43. data/lib/improve_your_code/report/formatter/heading_formatter.rb +33 -0
  44. data/lib/improve_your_code/report/formatter/progress_formatter.rb +25 -0
  45. data/lib/improve_your_code/report/formatter/simple_warning_formatter.rb +13 -0
  46. data/lib/improve_your_code/report/text_report.rb +76 -0
  47. data/lib/improve_your_code/smell_configuration.rb +48 -0
  48. data/lib/improve_your_code/smell_detectors.rb +12 -0
  49. data/lib/improve_your_code/smell_detectors/base_detector.rb +114 -0
  50. data/lib/improve_your_code/smell_detectors/long_parameter_list.rb +44 -0
  51. data/lib/improve_your_code/smell_detectors/too_many_constants.rb +54 -0
  52. data/lib/improve_your_code/smell_detectors/too_many_instance_variables.rb +48 -0
  53. data/lib/improve_your_code/smell_detectors/too_many_methods.rb +47 -0
  54. data/lib/improve_your_code/smell_detectors/too_many_statements.rb +43 -0
  55. data/lib/improve_your_code/smell_detectors/uncommunicative_method_name.rb +58 -0
  56. data/lib/improve_your_code/smell_detectors/uncommunicative_module_name.rb +71 -0
  57. data/lib/improve_your_code/smell_detectors/uncommunicative_variable_name.rb +129 -0
  58. data/lib/improve_your_code/smell_detectors/unused_parameters.rb +24 -0
  59. data/lib/improve_your_code/smell_detectors/unused_private_method.rb +69 -0
  60. data/lib/improve_your_code/smell_warning.rb +70 -0
  61. data/lib/improve_your_code/source/source_code.rb +84 -0
  62. data/lib/improve_your_code/source/source_locator.rb +38 -0
  63. data/lib/improve_your_code/tree_dresser.rb +74 -0
  64. data/lib/improve_your_code/version.rb +7 -0
  65. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ImproveYourCode
4
+ module Report
5
+ module Formatter
6
+ class SimpleWarningFormatter
7
+ def format(warning)
8
+ "#{warning.lines.sort.inspect}:#{warning.base_message}"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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'