leftovers 0.1.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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.leftovers.yml +19 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +216 -0
  6. data/.ruby-version +1 -0
  7. data/.spellr.yml +13 -0
  8. data/.spellr_wordlists/english.txt +21 -0
  9. data/.spellr_wordlists/lorem.txt +1 -0
  10. data/.spellr_wordlists/ruby.txt +67 -0
  11. data/.spellr_wordlists/shell.txt +3 -0
  12. data/.travis.yml +10 -0
  13. data/Configuration.md +427 -0
  14. data/Gemfile +6 -0
  15. data/LICENSE.txt +21 -0
  16. data/README.md +172 -0
  17. data/Rakefile +16 -0
  18. data/bin/console +15 -0
  19. data/bin/setup +8 -0
  20. data/exe/leftovers +6 -0
  21. data/leftovers.gemspec +42 -0
  22. data/lib/config/attr_encrypted.yml +16 -0
  23. data/lib/config/builder.yml +3 -0
  24. data/lib/config/capistrano.yml +3 -0
  25. data/lib/config/datagrid.yml +9 -0
  26. data/lib/config/flipper.yml +3 -0
  27. data/lib/config/graphql.yml +20 -0
  28. data/lib/config/guard.yml +3 -0
  29. data/lib/config/haml.yml +2 -0
  30. data/lib/config/jbuilder.yml +2 -0
  31. data/lib/config/okcomputer.yml +3 -0
  32. data/lib/config/parser.yml +91 -0
  33. data/lib/config/pry.yml +3 -0
  34. data/lib/config/rack.yml +2 -0
  35. data/lib/config/rails.yml +387 -0
  36. data/lib/config/rake.yml +5 -0
  37. data/lib/config/redcarpet.yml +38 -0
  38. data/lib/config/rollbar.yml +3 -0
  39. data/lib/config/rspec.yml +56 -0
  40. data/lib/config/ruby.yml +77 -0
  41. data/lib/config/selenium.yml +21 -0
  42. data/lib/config/simplecov.yml +2 -0
  43. data/lib/config/will_paginate.yml +14 -0
  44. data/lib/leftovers/argument_rule.rb +216 -0
  45. data/lib/leftovers/backports.rb +56 -0
  46. data/lib/leftovers/cli.rb +50 -0
  47. data/lib/leftovers/collector.rb +67 -0
  48. data/lib/leftovers/config.rb +53 -0
  49. data/lib/leftovers/core_ext.rb +32 -0
  50. data/lib/leftovers/definition.rb +70 -0
  51. data/lib/leftovers/definition_set.rb +44 -0
  52. data/lib/leftovers/erb.rb +20 -0
  53. data/lib/leftovers/file.rb +30 -0
  54. data/lib/leftovers/file_collector.rb +219 -0
  55. data/lib/leftovers/file_list.rb +24 -0
  56. data/lib/leftovers/haml.rb +24 -0
  57. data/lib/leftovers/hash_rule.rb +40 -0
  58. data/lib/leftovers/merged_config.rb +71 -0
  59. data/lib/leftovers/name_rule.rb +53 -0
  60. data/lib/leftovers/node.rb +182 -0
  61. data/lib/leftovers/rake_task.rb +62 -0
  62. data/lib/leftovers/reporter.rb +11 -0
  63. data/lib/leftovers/rule.rb +74 -0
  64. data/lib/leftovers/transform_rule.rb +171 -0
  65. data/lib/leftovers/value_rule.rb +56 -0
  66. data/lib/leftovers/version.rb +5 -0
  67. data/lib/leftovers.rb +127 -0
  68. metadata +281 -0
@@ -0,0 +1,56 @@
1
+ test_paths:
2
+ - /spec/
3
+
4
+ rules:
5
+ - skip: true
6
+ name:
7
+ # matchers
8
+ - diffable?
9
+ - actual
10
+ # formatters:
11
+ - start
12
+ - example_group_started
13
+ - example_started
14
+ - example_passed
15
+ - example_failed
16
+ - example_pending
17
+ - message
18
+ - stop
19
+ - start_dump
20
+ - dump_pending
21
+ - dump_failures
22
+ - dump_summary
23
+ - seed
24
+ - close
25
+ - name:
26
+ - have_attributes
27
+ - has_attributes?
28
+ - receive_messages
29
+ calls:
30
+ - keys: '*'
31
+ - name:
32
+ - receive
33
+ - respond_to
34
+ calls:
35
+ argument: 1
36
+ - name: define_negated_matcher
37
+ defines:
38
+ argument: 1
39
+ calls:
40
+ argument: 2
41
+ - name:
42
+ has_prefix: be_
43
+ calls:
44
+ itself: true
45
+ delete_prefix: be_
46
+ add_suffix: '?'
47
+ - name:
48
+ has_prefix: have_
49
+ calls:
50
+ itself: true
51
+ add_suffix: '?'
52
+ delete_prefix: have_
53
+ add_prefix: has_
54
+ - name: let
55
+ defines:
56
+ argument: 1
@@ -0,0 +1,77 @@
1
+ ---
2
+ include_paths:
3
+ - '*.rb'
4
+ - Gemfile
5
+ - '*.erb'
6
+ - '*.gemspec'
7
+ - .irbrc
8
+ - irb.rc
9
+ - _irbrc
10
+ - $irbrc
11
+
12
+ test_paths:
13
+ - /tests/
14
+ - /test/
15
+
16
+ rules:
17
+ - skip: true
18
+ name:
19
+ - initialize # called by new
20
+ - inspect # called by repl
21
+ - respond_to_missing? # called by respond_to?
22
+ - method_missing # called by method dispatch
23
+ - to_s # called by #{}
24
+ - to_ary
25
+ - to_str
26
+ - <=> # called by Comparable
27
+ - each # called by Enumerable
28
+ - marshal_dump # called by Marshal.dump
29
+ - marshal_load # called by Marshal.load
30
+ - _dump # called by Marshal.dump
31
+ - _load # called by Marshal.load
32
+ - name:
33
+ - send
34
+ - public_send
35
+ - __send__
36
+ - method
37
+ - respond_to?
38
+ - instance_method
39
+ calls:
40
+ argument: 1
41
+ - name: attr_reader
42
+ defines:
43
+ argument: '*'
44
+ calls:
45
+ argument: '*'
46
+ add_prefix: '@'
47
+ - name: attr_writer
48
+ defines:
49
+ argument: '*'
50
+ add_suffix: '='
51
+ - name: attr_accessor
52
+ defines:
53
+ - argument: '*'
54
+ transforms:
55
+ - original
56
+ - add_suffix: '='
57
+ calls:
58
+ argument: '*'
59
+ add_prefix: '@'
60
+ - name: define_method
61
+ defines:
62
+ argument: 1
63
+ - name: alias_method
64
+ defines:
65
+ argument: 1
66
+ calls:
67
+ argument: 2
68
+ - name:
69
+ - instance_variable_get
70
+ - class_variable_set
71
+ calls:
72
+ argument: 1
73
+ - name:
74
+ - instance_variable_set
75
+ - class_variable_set
76
+ defines:
77
+ argument: 1
@@ -0,0 +1,21 @@
1
+ rules:
2
+ skip: true
3
+ name:
4
+ - before_navigate_to
5
+ - before_navigate_back
6
+ - after_navigate_back
7
+ - before_navigate_forward
8
+ - after_navigate_forward
9
+ - before_find
10
+ - after_find
11
+ - before_click
12
+ - before_change_value_of
13
+ - after_change_value_of
14
+ - before_execute_script
15
+ - after_execute_script
16
+ - before_quit
17
+ - before_close
18
+ - after_navigate_to
19
+ - after_click
20
+ - after_quit
21
+ - after_close
@@ -0,0 +1,2 @@
1
+ include_paths:
2
+ - .simplecov
@@ -0,0 +1,14 @@
1
+ rules:
2
+ skip: true
3
+ name:
4
+ # WillPaginate::ViewHelpers::LinkRenderer
5
+ - page_number
6
+ - gap
7
+ - previous_page
8
+ - next_page
9
+ - previous_or_next_page
10
+ - html_container
11
+ - url
12
+ - prepare
13
+ - to_html
14
+ - container_attributes
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'definition'
4
+ require_relative 'definition_set'
5
+ require_relative 'name_rule'
6
+ require_relative 'transform_rule'
7
+ require_relative 'hash_rule'
8
+
9
+ module Leftovers
10
+ class ArgumentRule # rubocop:disable Metrics/ClassLength
11
+ def self.wrap(rules, definer: false) # rubocop:disable Metrics/MethodLength
12
+ case rules
13
+ when Array
14
+ rules.flat_map { |r| wrap(r, definer: definer) }
15
+ when Hash
16
+ [new(**rules, definer: definer)]
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ ADDITIONAL_VALID_KEYS = Leftovers::TransformRule::VALID_TRANSFORMS + %i{if unless}
23
+ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength, Metrics/CyclomaticComplexity
24
+ argument: nil,
25
+ arguments: nil,
26
+ key: nil,
27
+ keys: nil,
28
+ itself: false,
29
+ linked_transforms: nil,
30
+ transforms: nil,
31
+ definer: false,
32
+ **options
33
+ )
34
+ assert_valid_keys(options, ADDITIONAL_VALID_KEYS)
35
+ prepare_argument(argument, arguments)
36
+ @key = prepare_key(key, keys)
37
+ @itself = itself
38
+
39
+ unless @positions || @keywords || @all_positions || @all_keywords || @key || @itself
40
+ raise ArgumentError, "require at least one of 'argument(s)', 'key(s)', itself"
41
+ end
42
+
43
+ @if = prepare_condition(options.delete(:if))
44
+ @unless = prepare_condition(options.delete(:unless))
45
+ @transforms = prepare_transform(options, transforms, linked_transforms)
46
+ @definer = definer
47
+ end
48
+
49
+ def prepare_transform(options, transforms, linked_transforms) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
50
+ if linked_transforms && transforms
51
+ raise ArgumentError, 'Only use one of linked_transforms/transforms'
52
+ end
53
+ return if !linked_transforms && !transforms && options.empty?
54
+
55
+ if !(linked_transforms || transforms)
56
+ @transform = TransformRule.new(options)
57
+ else
58
+ @linked = !!linked_transforms
59
+
60
+ transforms = (linked_transforms || transforms).map do |transform|
61
+ transform = { transform.to_sym => true } if transform.is_a?(String)
62
+ Leftovers::TransformRule.new(options.merge(transform))
63
+ end
64
+
65
+ if transforms.length <= 1
66
+ @transform = transforms.first
67
+ else
68
+ @transforms = transforms
69
+ end
70
+ end
71
+ end
72
+
73
+ def prepare_condition(conditions)
74
+ Array.wrap(conditions).each do |cond|
75
+ cond[:has_argument] = HashRule.new(cond[:has_argument]) if cond[:has_argument]
76
+ end
77
+ end
78
+
79
+ def prepare_key(key, keys)
80
+ raise ArgumentError, 'Only use one of key/keys' if key && keys
81
+
82
+ key || keys
83
+ end
84
+
85
+ def prepare_argument(argument, arguments) # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
86
+ raise ArgumentError, 'Only use one of argument/arguments' if argument && arguments
87
+
88
+ positions = Set.new
89
+ keywords = []
90
+
91
+ Array.each_or_self(argument || arguments) do |arg|
92
+ case arg
93
+ when '*'
94
+ @all_positions = true
95
+ when '**'
96
+ @all_keywords = true
97
+ when Integer
98
+ positions << arg - 1
99
+ when String, Symbol, Hash
100
+ keywords << arg
101
+ end
102
+ end
103
+
104
+ @positions = positions unless @all_positions || positions.empty? || @all_positions
105
+ @keywords = NameRule.new(keywords) unless @all_keywords || keywords.empty?
106
+ end
107
+
108
+ def matches(method_node) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
109
+ return Array::EMPTY unless all_conditions_match?(method_node)
110
+
111
+ result = []
112
+
113
+ if @all_positions
114
+ result.gather values(method_node.positional_arguments, method_node)
115
+ elsif @positions
116
+ result.gather values(method_node.positional_arguments_at(@positions).compact, method_node)
117
+ end
118
+
119
+ if @keywords || @all_keywords || @key
120
+ result.gather hash_values(method_node.kwargs, method_node)
121
+ end
122
+ result << method_value(method_node) if @itself
123
+
124
+ result
125
+ end
126
+
127
+ def values(value_nodes, method_node)
128
+ value_nodes.flat_map { |value_node| value(value_node, method_node) }.compact
129
+ end
130
+
131
+ def all_conditions_match?(method_node)
132
+ @if.all? { |c| condition_match?(c, method_node) } &&
133
+ @unless.all? { |c| !condition_match?(c, method_node) }
134
+ end
135
+
136
+ def condition_match?(condition, method_name)
137
+ return unless condition[:has_argument]
138
+
139
+ hash_node = method_name.kwargs
140
+ return false unless hash_node
141
+
142
+ hash_node.each_pair.any? { |key, value| condition[:has_argument].match_pair?(key, value) }
143
+ end
144
+
145
+ def hash_values(hash_node, method_node) # rubocop:disable Metrics/MethodLength
146
+ return unless hash_node
147
+
148
+ value_nodes = []
149
+ value_nodes.concat hash_node.keys if @key
150
+
151
+ if @all_keywords
152
+ value_nodes.concat(hash_node.values)
153
+ elsif @keywords
154
+ value_nodes.concat(hash_node.values_at_match(@keywords))
155
+ end
156
+
157
+ values(value_nodes, method_node)
158
+ end
159
+
160
+ def value(value_node, method_node) # rubocop:disable Metrics/MethodLength
161
+ return unless value_node
162
+
163
+ value_node = value_node.unwrap_freeze
164
+
165
+ case value_node.type
166
+ when :array
167
+ values(value_node.values, method_node)
168
+ when :hash
169
+ hash_values(value_node, method_node)
170
+ when :str, :sym
171
+ symbol_values(value_node, method_node)
172
+ end
173
+ end
174
+
175
+ SPLIT = /[.:]+/.freeze
176
+ def symbol_values(symbol_node, method_node) # rubocop:disable Metrics/MethodLength
177
+ subnodes = symbol_node.to_s.split(SPLIT).flat_map { |s| transform(s, method_node) }
178
+
179
+ return subnodes unless @definer
180
+
181
+ location = symbol_node.loc.expression
182
+ if @linked
183
+ Leftovers::DefinitionSet.new(subnodes, location: location, method_node: method_node)
184
+ else
185
+ subnodes.map do |subnode|
186
+ Leftovers::Definition.new(subnode, location: location, method_node: method_node)
187
+ end
188
+ end
189
+ end
190
+
191
+ def method_value(method_node)
192
+ value = transform(method_node.to_s, method_node)
193
+
194
+ return value unless @definer
195
+
196
+ Leftovers::Definition.new(value, method_node: method_node)
197
+ end
198
+
199
+ def transform(string, method_node)
200
+ return string.to_sym unless @transform || @transforms
201
+ return @transform.transform(string, method_node) if @transform
202
+
203
+ @transforms.map do |transform|
204
+ transform.transform(string, method_node)
205
+ end
206
+ end
207
+
208
+ def assert_valid_keys(options, keys)
209
+ invalid = options.keys - keys
210
+
211
+ return if invalid.empty?
212
+
213
+ raise ArgumentError, "unknown keyword#{'s' if invalid.length > 1}: #{invalid.join(', ')}"
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leftovers
4
+ ruby_version = Gem::Version.new(RUBY_VERSION)
5
+ unless ruby_version >= Gem::Version.new('2.5')
6
+ module StringDeletePrefixSuffix
7
+ refine ::String do
8
+ def delete_prefix!(str)
9
+ slice!(0..(str.length - 1)) if start_with?(str)
10
+ self
11
+ end
12
+
13
+ def delete_suffix!(str)
14
+ slice!(-str.length..-1) if end_with?(str)
15
+ self
16
+ end
17
+
18
+ def delete_prefix(str)
19
+ dup.delete_prefix!(str)
20
+ end
21
+
22
+ def delete_suffix(str)
23
+ dup.delete_suffix!(str)
24
+ end
25
+ end
26
+ end
27
+
28
+ require 'yaml'
29
+ module YAMLSymbolizeNames
30
+ refine YAML.singleton_class do
31
+ alias_method :safe_load_without_symbolize_names, :safe_load
32
+ def safe_load(path, *args, symbolize_names: false, **kwargs)
33
+ if symbolize_names
34
+ symbolize_names!(safe_load_without_symbolize_names(path, *args, **kwargs))
35
+ else
36
+ safe_load_without_symbolize_names(path, *args, **kwargs)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def symbolize_names!(obj) # rubocop:disable Metrics/MethodLength
43
+ case obj
44
+ when Hash
45
+ obj.keys.each do |key| # rubocop:disable Style/HashEachMethods # each_key never finishes.
46
+ obj[key.to_sym] = symbolize_names!(obj.delete(key))
47
+ end
48
+ when Array
49
+ obj.map! { |ea| symbolize_names!(ea) }
50
+ end
51
+ obj
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require_relative '../leftovers'
5
+ require_relative 'version'
6
+
7
+ module Leftovers
8
+ class CLI
9
+ attr_reader :argv, :stdout, :stderr
10
+
11
+ def initialize(argv: [], stdout: StringIO.new, stderr: StringIO.new)
12
+ @argv = argv
13
+ @stdout = stdout
14
+ @stderr = stderr
15
+
16
+ parse_options
17
+
18
+ exit Leftovers.run(stdout: stdout, stderr: stderr)
19
+ end
20
+
21
+ def parse_options # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
22
+ opts = OptionParser.new
23
+ Leftovers.parallel = true
24
+ Leftovers.progress = true
25
+
26
+ opts.banner = 'Usage: leftovers [options]'
27
+ opts.on('-q', '--quiet', 'Silences output') { Leftovers.quiet = true }
28
+ opts.on('--[no-]parallel', 'Run in parallel or not, default --parallel') do |p|
29
+ Leftovers.parallel = p
30
+ end
31
+ opts.on('--[no-]progress', 'Show progress counts or not, default --progress') do |p|
32
+ Leftovers.progress = p
33
+ end
34
+ opts.on('-v', '--version', 'Returns the current version') do
35
+ stdout.puts(Leftovers::Version)
36
+ exit
37
+ end
38
+ opts.on('--dry-run', 'Output files that will be looked at') do
39
+ stdout.puts(Leftovers::FileList.new.to_a.map(&:relative_path).join("\n"))
40
+ exit
41
+ end
42
+ opts.on('-h', '--help', 'Shows this message') do
43
+ stdout.puts(opts.help)
44
+ exit
45
+ end
46
+
47
+ opts.parse(argv)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast_ignore'
4
+ require 'set'
5
+ require 'parallel'
6
+ require_relative 'file_collector'
7
+ require_relative 'file_list'
8
+
9
+ module Leftovers
10
+ class Collector
11
+ attr_reader :calls
12
+ attr_reader :test_calls
13
+ attr_reader :definitions
14
+
15
+ def initialize # rubocop:disable Metrics/MethodLength
16
+ @calls = []
17
+ @test_calls = []
18
+ @definitions = []
19
+ @count = 0
20
+ @count_calls = 0
21
+ @count_definitions = 0
22
+ end
23
+
24
+ def collect
25
+ collect_file_list(Leftovers::FileList.new)
26
+ print_progress
27
+ Leftovers.newline
28
+ @calls = @calls.to_set.freeze
29
+ @test_calls = @test_calls.to_set.freeze
30
+ end
31
+
32
+ def collect_file_list(list)
33
+ if Leftovers.parallel?
34
+ Parallel.each(list, finish: method(:finish_parallel), &method(:collect_file))
35
+ else
36
+ list.each { |file| finish_parallel(nil, nil, collect_file(file)) }
37
+ end
38
+ end
39
+
40
+ def collect_file(file)
41
+ file_collector = Leftovers::FileCollector.new(file.ruby, file)
42
+ file_collector.collect
43
+
44
+ file_collector.to_h
45
+ end
46
+
47
+ def print_progress
48
+ Leftovers.print(
49
+ "checked #{@count} files, collected #{@count_calls} calls, #{@count_definitions} definitions\r" # rubocop:disable Layout/LineLength
50
+ )
51
+ end
52
+
53
+ def finish_parallel(_, _, result) # rubocop:disable Metrics/MethodLength
54
+ @count += 1
55
+ @count_calls += result[:calls].length
56
+ @count_definitions += result[:definitions].length
57
+ print_progress if Leftovers.progress?
58
+ if result[:test?]
59
+ @test_calls.concat(result[:calls])
60
+ else
61
+ @calls.concat(result[:calls])
62
+ end
63
+
64
+ @definitions.concat(result[:definitions])
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative 'rule'
5
+
6
+ module Leftovers
7
+ class Config
8
+ # :nocov:
9
+ using ::Leftovers::YAMLSymbolizeNames if defined?(::Leftovers::YAMLSymbolizeNames)
10
+ # :nocov:
11
+
12
+ attr_reader :name
13
+
14
+ def initialize(
15
+ name,
16
+ path: ::File.join(__dir__, '..', 'config', "#{name}.yml"),
17
+ content: (::File.exist?(path) ? ::File.read(path) : '')
18
+ )
19
+ @name = name.to_sym
20
+ @path = path
21
+ @content = content
22
+ end
23
+
24
+ def gems
25
+ @gems ||= Array(yaml[:gems]).map(&:to_sym)
26
+ end
27
+
28
+ def exclude_paths
29
+ @exclude_paths ||= Array(yaml[:exclude_paths])
30
+ end
31
+
32
+ def include_paths
33
+ @include_paths ||= Array(yaml[:include_paths])
34
+ end
35
+
36
+ def test_paths
37
+ @test_paths ||= Array(yaml[:test_paths])
38
+ end
39
+
40
+ def rules
41
+ @rules ||= Rule.wrap(yaml[:rules])
42
+ end
43
+
44
+ private
45
+
46
+ def yaml
47
+ @yaml ||= YAML.safe_load(@content, symbolize_names: true) || {}
48
+ rescue Psych::SyntaxError => e
49
+ warn "\e[31mError with config #{path}: #{e.message}\e[0m"
50
+ exit 1
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class Array
6
+ # concat, # push, # add, #
7
+ EMPTY = [].freeze
8
+
9
+ def gather(other)
10
+ case other
11
+ when Array, Set then concat(other)
12
+ when nil then self
13
+ else self.<< other
14
+ end
15
+ end
16
+
17
+ def self.wrap(value)
18
+ case value
19
+ when nil then EMPTY
20
+ when Array then value
21
+ else [value]
22
+ end
23
+ end
24
+
25
+ def self.each_or_self(value, &block)
26
+ case value
27
+ when nil then nil
28
+ when Array then value.each(&block)
29
+ else block.call(value)
30
+ end
31
+ end
32
+ end