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