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,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leftovers
4
+ class Definition
5
+ attr_reader :name
6
+ alias_method :names, :name
7
+ alias_method :full_name, :name
8
+ attr_reader :name_s
9
+ alias_method :to_s, :name_s
10
+ attr_reader :test
11
+ alias_method :test?, :test
12
+
13
+ def initialize( # rubocop:disable Metrics/MethodLength
14
+ name,
15
+ method_node: nil,
16
+ location: method_node.loc.expression,
17
+ file: method_node.file,
18
+ test: method_node.test?
19
+ )
20
+ @name = name
21
+ @name_s = name.to_s.freeze
22
+
23
+ @location = location
24
+ @file = file
25
+ @test = test
26
+
27
+ freeze
28
+ end
29
+
30
+ def <=>(other)
31
+ (path <=> other.path).nonzero? ||
32
+ (line <=> other.line).nonzero? ||
33
+ (column <=> other.column)
34
+ end
35
+
36
+ def path
37
+ @file.relative_path
38
+ end
39
+
40
+ def line
41
+ @location.line
42
+ end
43
+
44
+ def column
45
+ @location.column
46
+ end
47
+
48
+ def full_location
49
+ "#{path}:#{@location.line}:#{@location.column}"
50
+ end
51
+
52
+ def highlighted_source(highlight = "\e[31m", normal = "\e[0m") # rubocop:disable Metrics/AbcSize
53
+ @location.source_line.to_s[0...(@location.column_range.begin)].lstrip +
54
+ highlight + @location.source.to_s + normal +
55
+ @location.source_line.to_s[(@location.column_range.end)..-1].rstrip
56
+ end
57
+
58
+ def in_collection?
59
+ Leftovers.collector.calls.include?(@name) || (@test && in_test_collection?)
60
+ end
61
+
62
+ def in_test_collection?
63
+ Leftovers.collector.test_calls.include?(@name)
64
+ end
65
+
66
+ def skipped?
67
+ Leftovers.config.skip_rules.any? { |r| r.match?(@name, @name_s, path) }
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'definition'
4
+ module Leftovers
5
+ class DefinitionSet < Leftovers::Definition
6
+ def initialize( # rubocop:disable Metrics/MethodLength
7
+ names,
8
+ method_node: nil,
9
+ location: method_node.loc.expression,
10
+ file: method_node.file,
11
+ test: method_node.test?
12
+ )
13
+ @definitions = names.map do |name|
14
+ Leftovers::Definition.new(name, test: test, location: location, file: file)
15
+ end
16
+
17
+ @test = test
18
+ @location = location
19
+ @file = file
20
+
21
+ freeze
22
+ end
23
+
24
+ def names
25
+ @definitions.map(&:names)
26
+ end
27
+
28
+ def to_s
29
+ @definitions.map(&:to_s).join(', ')
30
+ end
31
+
32
+ def in_collection?
33
+ @definitions.any?(&:in_collection?)
34
+ end
35
+
36
+ def in_test_collection?
37
+ @definitions.any?(&:in_test_collection?)
38
+ end
39
+
40
+ def skipped?
41
+ @definitions.any?(&:skipped?)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Leftovers
6
+ class ERB < ::ERB::Compiler
7
+ def self.precompile(erb)
8
+ @compiler ||= new('-')
9
+ @compiler.compile(erb).first
10
+ end
11
+
12
+ def add_insert_cmd(out, content)
13
+ out.push("#{content}\n")
14
+ end
15
+
16
+ def add_put_cmd(out, _content)
17
+ out
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'erb'
4
+ require_relative 'haml'
5
+ require 'pathname'
6
+
7
+ module Leftovers
8
+ class File < Pathname
9
+ def relative_path
10
+ @relative_path ||= relative_path_from(Leftovers.pwd)
11
+ end
12
+
13
+ def test?
14
+ return @test if defined?(@test)
15
+
16
+ @test = Leftovers.config.test_paths.allowed?(relative_path)
17
+ end
18
+
19
+ def ruby # rubocop:disable Metrics/MethodLength
20
+ case extname
21
+ when '.haml'
22
+ Leftovers::Haml.precompile(read)
23
+ when '.rhtml', '.rjs', '.erb'
24
+ Leftovers::ERB.precompile(read)
25
+ else
26
+ read
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast_ignore'
4
+ require 'set'
5
+ require 'parser'
6
+ require 'parser/current'
7
+ require_relative 'node'
8
+ require_relative 'definition'
9
+
10
+ module Leftovers
11
+ class FileCollector < Parser::AST::Processor # rubocop:disable Metrics/ClassLength
12
+ attr_reader :calls
13
+ attr_reader :definitions
14
+
15
+ def initialize(ruby, file) # rubocop:disable Metrics/MethodLength
16
+ @calls = []
17
+ @definitions = []
18
+ @allow_lines = Set.new.compare_by_identity
19
+ @test_lines = Set.new.compare_by_identity
20
+ @ruby = ruby
21
+ @file = file
22
+ end
23
+
24
+ def filename
25
+ @filename ||= @file.relative_path
26
+ end
27
+
28
+ def to_h
29
+ {
30
+ test?: @file.test?,
31
+ calls: calls,
32
+ definitions: definitions
33
+ }
34
+ end
35
+
36
+ def collect
37
+ ast, comments = Parser::CurrentRuby.parse_with_comments(@ruby)
38
+ process_comments(comments)
39
+ process(ast)
40
+ rescue Parser::SyntaxError => e
41
+ Leftovers.warn "#{e.class}: #{e.message} #{filename}:#{e.diagnostic.location.line}:#{e.diagnostic.location.column}" # rubocop:disable Layout/LineLength
42
+ end
43
+
44
+ METHOD_NAME_RE = /[[:alpha:]_][[:alnum:]_]*\b[\?!=]?/.freeze
45
+ NON_ALNUM_METHOD_NAME_RE = Regexp.union(%w{
46
+ []= [] ** ~ +@ -@ * / % + - >> << &
47
+ ^ | <=> <= >= < > === == != =~ !~ !
48
+ }.map { |op| /#{Regexp.escape(op)}/ })
49
+ CONSTANT_NAME_RE = /[[:upper:]][[:alnum:]_]*\b/.freeze
50
+ NAME_RE = Regexp.union(METHOD_NAME_RE, NON_ALNUM_METHOD_NAME_RE, CONSTANT_NAME_RE)
51
+ LEFTOVERS_CALL_RE = /\bleftovers:call(?:s|ed|er|ers|) (#{NAME_RE}(?:[, :]+#{NAME_RE})*)/.freeze
52
+ LEFTOVERS_ALLOW_RE = /\bleftovers:(?:keeps?|skip(?:s|ped|)|allow(?:s|ed|))\b/.freeze
53
+ LEFTOVERS_TEST_RE = /\bleftovers:(?:for_tests?|tests?|testing)\b/.freeze
54
+ def process_comments(comments) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
55
+ comments.each do |comment|
56
+ @allow_lines << comment.loc.line if comment.text.match?(LEFTOVERS_ALLOW_RE)
57
+
58
+ @test_lines << comment.loc.line if comment.text.match?(LEFTOVERS_TEST_RE)
59
+
60
+ next unless (match = comment.text.match(LEFTOVERS_CALL_RE))
61
+ next unless match[1]
62
+
63
+ match[1].scan(NAME_RE).each { |s| add_call(s.to_sym) }
64
+ end
65
+ end
66
+
67
+ # grab method definitions
68
+ def on_def(node)
69
+ add_definition(node.children.first, node.loc.name)
70
+
71
+ super
72
+ end
73
+
74
+ def on_ivasgn(node)
75
+ add_definition(node.children.first, node.loc.name)
76
+
77
+ super
78
+ end
79
+ alias_method :on_gvasgn, :on_ivasgn
80
+ alias_method :on_cvasgn, :on_ivasgn
81
+
82
+ def on_ivar(node)
83
+ add_call(node.children.first)
84
+
85
+ super
86
+ end
87
+ alias_method :on_gvar, :on_ivar
88
+ alias_method :on_cvar, :on_ivar
89
+
90
+ def on_op_asgn(node)
91
+ collect_op_asgn(node)
92
+
93
+ super
94
+ end
95
+
96
+ def on_and_asgn(node)
97
+ collect_op_asgn(node)
98
+
99
+ super
100
+ end
101
+
102
+ def on_or_asgn(node)
103
+ collect_op_asgn(node)
104
+
105
+ super
106
+ end
107
+
108
+ # grab method calls
109
+ def on_send(node)
110
+ super
111
+
112
+ add_call(node.children[1])
113
+
114
+ collect_rules(node)
115
+ end
116
+ alias_method :on_csend, :on_send
117
+
118
+ def on_const(node)
119
+ super
120
+
121
+ add_call(node.children[1])
122
+ end
123
+
124
+ # grab e.g. :to_s in each(&:to_s)
125
+ def on_block_pass(node)
126
+ super
127
+
128
+ add_call(node.children.first.to_sym) if node.children.first&.string_or_symbol?
129
+ end
130
+
131
+ # grab class Constant or module Constant
132
+ def on_class(node)
133
+ # don't call super so we don't process the class name
134
+ # !!! (# wtf does this mean dana? what would happen instead?)
135
+ process_all(node.children.drop(1))
136
+
137
+ node = node.children.first
138
+
139
+ add_definition(node.children[1], node.loc.name)
140
+ end
141
+ alias_method :on_module, :on_class
142
+
143
+ # grab Constant = Class.new or CONSTANT = 'string'.freeze
144
+ def on_casgn(node)
145
+ super
146
+
147
+ add_definition(node.children[1], node.loc.name)
148
+
149
+ collect_rules(node)
150
+ end
151
+
152
+ # grab calls to `alias new_method original_method`
153
+ def on_alias(node)
154
+ super
155
+
156
+ new_method, original_method = node.children
157
+
158
+ add_definition(new_method.children.first, new_method.loc.expression)
159
+ add_call(original_method.children.first)
160
+ end
161
+
162
+ private
163
+
164
+ def test?(loc)
165
+ @file.test? || @test_lines.include?(loc.line)
166
+ end
167
+
168
+ def add_definition(name, loc)
169
+ return if @allow_lines.include?(loc.line)
170
+
171
+ definitions << Leftovers::Definition.new(name, location: loc, file: @file, test: test?(loc))
172
+ end
173
+
174
+ def add_call(name)
175
+ calls << name
176
+ end
177
+
178
+ # just collects the call, super will collect the definition
179
+ def collect_var_op_asgn(node)
180
+ name = node.children.first
181
+
182
+ return unless name
183
+
184
+ add_call(name)
185
+ end
186
+
187
+ def collect_send_op_asgn(node)
188
+ name = node.children[1]
189
+
190
+ return unless name
191
+
192
+ add_call(:"#{name}=")
193
+ end
194
+
195
+ def collect_op_asgn(node)
196
+ node = node.children.first
197
+ case node.type
198
+ when :send then collect_send_op_asgn(node)
199
+ when :ivasgn, :gvasgn, :cvasgn then collect_var_op_asgn(node)
200
+ end
201
+ end
202
+
203
+ def collect_rules(node) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
204
+ Leftovers.config.rules.each do |rule|
205
+ next unless rule.match?(node.name, node.name_s, filename)
206
+
207
+ next if rule.skip?
208
+
209
+ calls.concat(rule.calls(node))
210
+
211
+ next if @allow_lines.include?(node.loc.line)
212
+
213
+ node.file = @file
214
+ node.test = test?(node.loc)
215
+ definitions.concat(rule.definitions(node))
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast_ignore'
4
+ require_relative 'file'
5
+
6
+ module Leftovers
7
+ class FileList
8
+ include Enumerable
9
+
10
+ def each # rubocop:disable Metrics/MethodLength
11
+ FastIgnore.new(
12
+ ignore_rules: Leftovers.config.exclude_paths,
13
+ include_rules: Leftovers.config.include_paths,
14
+ include_shebangs: :ruby
15
+ ).each do |file|
16
+ yield(Leftovers::File.new(file))
17
+ end
18
+ end
19
+
20
+ def to_a
21
+ enum_for(:each).to_a
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leftovers
4
+ module Haml
5
+ module_function
6
+
7
+ def precompile(file) # rubocop:disable Metrics/MethodLength
8
+ Leftovers.try_require('haml', message: <<~MESSAGE)
9
+ Skipped parsing a haml file, because the haml gem was not available
10
+ `gem install Haml`
11
+ MESSAGE
12
+ if defined?(::Haml)
13
+ begin
14
+ ::Haml::Engine.new(file).precompiled
15
+ rescue ::Haml::SyntaxError => e
16
+ Leftovers.warn "#{e.class}: #{e.message} #{filename}:#{e.line}"
17
+ ''
18
+ end
19
+ else
20
+ ''
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require_relative 'value_rule'
5
+ require_relative 'name_rule'
6
+
7
+ module Leftovers
8
+ class HashRule
9
+ def initialize(patterns) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
10
+ keys = []
11
+ pairs = []
12
+ Array.each_or_self(patterns) do |pat|
13
+ if pat.is_a?(Hash) && pat[:value]
14
+ pairs << [
15
+ (NameRule.new(pat[:keyword]) if pat[:keyword]),
16
+ (ValueRule.new(pat[:value]) if pat[:value])
17
+ ]
18
+ else
19
+ keys << NameRule.new(pat)
20
+ end
21
+ end
22
+
23
+ @keys = (NameRule.new(keys) if keys)
24
+
25
+ @pairs = (pairs unless pairs.empty?)
26
+
27
+ freeze
28
+ end
29
+
30
+ def match_pair?(key_node, value_node)
31
+ return true if @keys&.match?(key_node.to_sym, key_node.to_s)
32
+
33
+ @pairs&.any? do |(key_rule, value_rule)|
34
+ next unless !key_rule || key_rule.match?(key_node.to_sym, key_node.to_s)
35
+
36
+ (!value_rule || value_rule.match?(value_node))
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require_relative 'config'
5
+ require 'fast_ignore'
6
+
7
+ module Leftovers
8
+ class MergedConfig
9
+ def initialize
10
+ @configs = []
11
+ @loaded_configs = Set.new
12
+ self.<< Leftovers::Config.new(:ruby)
13
+ self.<< project_config
14
+ load_bundled_gem_config
15
+ end
16
+
17
+ def <<(config)
18
+ return if @loaded_configs.include?(config.name)
19
+
20
+ unmemoize
21
+ @configs << config
22
+ @loaded_configs << config.name
23
+ config.gems.each { |gem| self.<< Leftovers::Config.new(gem) }
24
+ end
25
+
26
+ def project_config
27
+ Leftovers::Config.new(:'.leftovers.yml', path: Leftovers.pwd + '.leftovers.yml')
28
+ end
29
+
30
+ def unmemoize
31
+ remove_instance_variable(:@exclude_paths) if defined?(@exclude_paths)
32
+ remove_instance_variable(:@include_paths) if defined?(@include_paths)
33
+ remove_instance_variable(:@test_paths) if defined?(@test_paths)
34
+ remove_instance_variable(:@rules) if defined?(@rules)
35
+ end
36
+
37
+ def exclude_paths
38
+ @exclude_paths ||= @configs.flat_map(&:exclude_paths)
39
+ end
40
+
41
+ def include_paths
42
+ @include_paths ||= @configs.flat_map(&:include_paths)
43
+ end
44
+
45
+ def test_paths
46
+ @test_paths ||= FastIgnore.new(
47
+ include_rules: @configs.flat_map(&:test_paths),
48
+ gitignore: false
49
+ )
50
+ end
51
+
52
+ def skip_rules
53
+ @skip_rules ||= rules.select(&:skip?)
54
+ end
55
+
56
+ def rules
57
+ @rules ||= @configs.flat_map(&:rules)
58
+ end
59
+
60
+ private
61
+
62
+ def load_bundled_gem_config
63
+ Leftovers.try_require('bundler')
64
+ return unless defined?(Bundler)
65
+
66
+ Bundler.locked_gems.specs.each do |spec|
67
+ self.<< Leftovers::Config.new(spec.name.to_sym)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ module Leftovers
5
+ class NameRule
6
+ attr_reader :sym, :syms, :regexp
7
+
8
+ def initialize(patterns) # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
9
+ regexps = []
10
+ syms = Set.new
11
+ Array.each_or_self(patterns) do |pat|
12
+ case pat
13
+ when Leftovers::NameRule
14
+ syms.merge(pat.sym) if pat.sym
15
+ syms.merge(pat.syms) if pat.syms
16
+ regexps.concat(pat.regexp) if pat.regexp
17
+ when String
18
+ syms.merge(pat.split(/\s+/).map(&:to_sym))
19
+ when Hash
20
+ if pat[:match]
21
+ regexps << /\A#{pattern[:match]}\z/
22
+ elsif pat[:has_prefix] && pat[:has_suffix]
23
+ regexps << /\A#{Regexp.escape(pat[:has_prefix])}.*#{Regexp.escape(pat[:has_suffix])}\z/
24
+ elsif pat[:has_prefix]
25
+ regexps << /\A#{Regexp.escape(pat[:has_prefix])}/
26
+ elsif pat[:has_suffix]
27
+ regexps << /#{Regexp.escape(pat[:has_suffix])}\z/
28
+ end
29
+ end
30
+ end
31
+
32
+ if syms.length <= 0
33
+ @sym = syms.first
34
+ @syms = nil
35
+ else
36
+ @sym = nil
37
+ @syms = syms
38
+ end
39
+
40
+ @regexp = if regexps.empty?
41
+ nil
42
+ else
43
+ Regexp.union(regexps)
44
+ end
45
+
46
+ freeze
47
+ end
48
+
49
+ def match?(sym, string)
50
+ @sym&.==(sym) || @syms&.include?(sym) || @regexp&.match?(string)
51
+ end
52
+ end
53
+ end