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,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
+ ]