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,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser'
4
+
5
+ module Parser
6
+ module AST
7
+ class Node # rubocop:disable Metrics/ClassLength
8
+ def initialize(type, children = [], properties = {})
9
+ # ::AST::Node#initialize freezes itself.
10
+ # so can't use normal memoizations
11
+ @memo = {}
12
+
13
+ super
14
+ end
15
+
16
+ def first
17
+ children.first
18
+ end
19
+
20
+ def file
21
+ @memo[:file]
22
+ end
23
+
24
+ def file=(value)
25
+ @memo[:file] = value
26
+ end
27
+
28
+ def test
29
+ @memo[:test]
30
+ end
31
+ alias_method :test?, :test
32
+
33
+ def test=(value)
34
+ @memo[:test] = value
35
+ end
36
+
37
+ def to_scalar_value # rubocop:disable Metrics/MethodLength
38
+ @memo[:scalar_value] ||= case type
39
+ when :sym
40
+ first
41
+ when :str
42
+ first.to_s.freeze
43
+ when :true
44
+ true
45
+ when :false
46
+ false
47
+ when :nil
48
+ nil
49
+ else
50
+ raise "Not scalar node, (#{type})"
51
+ end
52
+ end
53
+
54
+ def to_s # rubocop:disable Metrics/MethodLength
55
+ @memo[:to_s] ||= if scalar?
56
+ to_scalar_value
57
+ elsif named?
58
+ name
59
+ else
60
+ raise "No to_s, (#{type})"
61
+ end.to_s.freeze
62
+ end
63
+
64
+ def to_sym
65
+ case type
66
+ when :str, :sym then first
67
+ when :nil, :true, :false then type
68
+ else to_s.to_sym
69
+ end
70
+ end
71
+
72
+ SCALAR_TYPES = %i{sym str true false nil}.freeze
73
+ def scalar?
74
+ SCALAR_TYPES.include?(type)
75
+ end
76
+
77
+ def string_or_symbol?
78
+ type == :str || type == :sym
79
+ end
80
+
81
+ def send?
82
+ type == :send || type == :csend
83
+ end
84
+
85
+ def named?
86
+ send? || type == :casgn
87
+ end
88
+
89
+ def arguments
90
+ @memo[:arguments] ||= case type
91
+ when :send, :csend then children.drop(2)
92
+ when :casgn then [children[2]]
93
+ else raise "Not argument node (#{type})"
94
+ end
95
+ end
96
+
97
+ def positional_arguments
98
+ @memo[:positional_arguments] ||= kwargs ? arguments[0...-1] : arguments
99
+ end
100
+
101
+ def unwrap_freeze
102
+ return self unless type == :send && name == :freeze
103
+
104
+ first
105
+ end
106
+
107
+ def kwargs
108
+ @memo.fetch(:kwargs) do
109
+ last_arg = arguments[-1]&.unwrap_freeze
110
+ @memo[:kwargs] = (last_arg if last_arg&.type == :hash)
111
+ end
112
+ end
113
+
114
+ def keys
115
+ each_pair.map { |k, _| k }
116
+ end
117
+
118
+ def key?(key)
119
+ each_pair.find do |k, _v|
120
+ next unless k.string_or_symbol?
121
+
122
+ k.to_sym == key
123
+ end
124
+ end
125
+
126
+ def values
127
+ @memo[:kwargs] ||= case type
128
+ when :hash then each_pair.map { |_, v| v }
129
+ when :array then children
130
+ else []
131
+ end
132
+ end
133
+
134
+ def values_at_match(matcher)
135
+ each_pair.with_object([]) do |(key, value), values|
136
+ values << value if matcher.match?(key.to_sym, key.to_s)
137
+ end
138
+ end
139
+
140
+ def positional_arguments_at(positions)
141
+ positional_arguments.values_at(*positions).compact
142
+ end
143
+
144
+ def each_pair
145
+ raise "not hash node (#{type})" unless type == :hash
146
+
147
+ return enum_for(:each_pair) unless block_given?
148
+
149
+ children.each do |pair|
150
+ yield(*pair.children) if pair.type == :pair
151
+ end
152
+ end
153
+
154
+ def name
155
+ return "Not named node (#{type})" unless named?
156
+
157
+ @memo[:name] ||= children[1]
158
+ end
159
+
160
+ def name_s
161
+ @memo[:name_s] ||= name.to_s.freeze
162
+ end
163
+
164
+ def [](index) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
165
+ case type
166
+ when :send, :csend
167
+ index.is_a?(Integer) ? arguments[index] : kwargs && kwargs[index]
168
+ when :array
169
+ children[index]
170
+ when :hash
171
+ each_pair do |key, value|
172
+ next unless key.string_or_symbol?
173
+
174
+ return value if key.to_sym == index
175
+ end
176
+
177
+ nil
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+ require 'shellwords'
5
+ require_relative 'cli'
6
+
7
+ module Leftovers
8
+ class RakeTask
9
+ include ::Rake::DSL
10
+
11
+ def self.generate_task(name = :leftovers, *default_argv) # rubocop:disable
12
+ new(name, default_argv)
13
+
14
+ name
15
+ end
16
+
17
+ def initialize(name, default_argv)
18
+ @name = name
19
+ @default_argv = default_argv
20
+
21
+ describe_task
22
+ define_task
23
+ end
24
+
25
+ private
26
+
27
+ def escaped_argv(argv = @default_argv)
28
+ return if argv.empty?
29
+
30
+ Shellwords.shelljoin(argv)
31
+ end
32
+
33
+ def describe_task
34
+ return desc('Run leftovers') if @default_argv.empty?
35
+
36
+ desc("Run leftovers (default args: #{escaped_argv})")
37
+ end
38
+
39
+ def define_task
40
+ task(@name, :'*args') do |_, task_argv|
41
+ argv = argv_or_default(task_argv)
42
+ write_cli_cmd(argv)
43
+ run(argv)
44
+ end
45
+ end
46
+
47
+ def write_cli_cmd(argv)
48
+ $stdout.puts("\e[2mleftovers #{escaped_argv(argv)}\e[0m")
49
+ end
50
+
51
+ def run(argv)
52
+ Leftovers::CLI.new(argv: argv, stdout: $stdout, stderr: $stderr)
53
+ rescue SystemExit => e
54
+ raise unless e.status == 0
55
+ end
56
+
57
+ def argv_or_default(task_argv)
58
+ task_argv = task_argv.to_a.compact
59
+ task_argv.empty? ? @default_argv : task_argv
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leftovers
4
+ class Reporter
5
+ def call(definition)
6
+ Leftovers.puts(
7
+ "\e[36m#{definition.full_location}\e[0m #{definition.full_name} \e[2m#{definition.highlighted_source("\e[33m", "\e[0;2m")}\e[0m" # rubocop:disable Layout/LineLength
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'name_rule'
4
+ require_relative 'argument_rule'
5
+ require 'fast_ignore'
6
+
7
+ module Leftovers
8
+ class Rule
9
+ def self.wrap(rules)
10
+ case rules
11
+ when Array then rules.flat_map { |r| wrap(r) }
12
+ when nil then [].freeze
13
+ else new(**rules)
14
+ end
15
+ end
16
+
17
+ attr_reader :skip
18
+ alias_method :skip?, :skip
19
+
20
+ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
21
+ name: nil,
22
+ names: nil,
23
+ calls: nil,
24
+ call: nil,
25
+ skip: false,
26
+ defines: nil,
27
+ define: nil,
28
+ path: nil,
29
+ paths: nil
30
+ )
31
+ raise ArgumentError, 'Only use one of name/names' if name && names
32
+ raise ArgumentError, 'Only use one of path/paths' if path && paths
33
+ raise ArgumentError, 'Only use one of call/calls' if call && calls
34
+ raise ArgumentError, 'Only use one of define/defines' if define && defines
35
+ if skip && (defines || calls)
36
+ raise ArgumentError, "skip can't exist with defines or calls for #{name || names}"
37
+ end
38
+
39
+ @name_matcher = NameRule.new(name || names)
40
+ @path = FastIgnore.new(include_rules: path || paths, gitignore: false) if path || paths
41
+ @skip = skip
42
+
43
+ begin
44
+ @calls = ArgumentRule.wrap(calls)
45
+ rescue ArgumentError => e
46
+ raise e, "#{e.message} for calls for #{name}", e.backtrace
47
+ end
48
+
49
+ begin
50
+ @defines = ArgumentRule.wrap(defines, definer: true)
51
+ rescue ArgumentError => e
52
+ raise e, "#{e.message} for defines for #{name}", e.backtrace
53
+ end
54
+ end
55
+
56
+ def filename?(file)
57
+ return true unless @path
58
+
59
+ @path.allowed?(file)
60
+ end
61
+
62
+ def match?(name, name_s, file)
63
+ @name_matcher.match?(name, name_s) && filename?(file)
64
+ end
65
+
66
+ def calls(node)
67
+ @calls.flat_map { |m| m.matches(node) }
68
+ end
69
+
70
+ def definitions(node)
71
+ @defines.flat_map { |m| m.matches(node) }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leftovers
4
+ class TransformRule # rubocop:disable Metrics/ClassLength
5
+ # :nocov:
6
+ using ::Leftovers::StringDeletePrefixSuffix if defined?(::Leftovers::StringDeletePrefixSuffix)
7
+ # :nocov:
8
+
9
+ RUBY_STRING_METHODS = %i{
10
+ downcase
11
+ upcase
12
+ capitalize
13
+ swapcase
14
+ }.freeze
15
+
16
+ ACTIVESUPPORT_STRING_METHODS = %i{
17
+ pluralize
18
+ singularize
19
+ camelize
20
+ camelcase
21
+ underscore
22
+ titleize
23
+ titlecase
24
+ demodulize
25
+ deconstantize
26
+ parameterize
27
+ }.freeze
28
+
29
+ CUSTOM_TRANSFORMS = %i{
30
+ original
31
+ delete_before
32
+ delete_after
33
+ add_prefix
34
+ add_suffix
35
+ delete_prefix
36
+ delete_suffix
37
+ replace_with
38
+ }.freeze
39
+
40
+ VALID_TRANSFORMS = RUBY_STRING_METHODS + ACTIVESUPPORT_STRING_METHODS + CUSTOM_TRANSFORMS
41
+ # more possible transformations
42
+ # gsub sub tr tr_s
43
+ def initialize(transforms)
44
+ @transforms = prepare_transforms(transforms)
45
+
46
+ freeze
47
+ end
48
+
49
+ def transform(original_string, method_node)
50
+ string = original_string
51
+ @transforms.each { |proc| string = proc.call(string, method_node) }
52
+
53
+ string.to_sym
54
+ end
55
+
56
+ private
57
+
58
+ def prepare_transforms(transforms) # rubocop:disable Metrics/MethodLength
59
+ transforms.map do |key, value|
60
+ unless VALID_TRANSFORMS.include?(key)
61
+ raise ArgumentError, <<~MESSAGE
62
+ invalid transform key: (#{key}: #{value}).
63
+ Valid transform keys are #{ALL_VALID_TRANSFORMS}"
64
+ MESSAGE
65
+ end
66
+
67
+ key, value = prepare_hash_value(key, value) if value.is_a?(Hash)
68
+
69
+ instance_variable_set(:"@#{key}", value.freeze)
70
+ method(key)
71
+ end
72
+ end
73
+
74
+ HASH_VALUE_TRANSFORMS = %i{add_prefix add_suffix}.freeze
75
+ HASH_VALUE_KEYS = %i{from_argument joiner}.freeze
76
+ def prepare_hash_value(method, hash) # rubocop:disable Metrics/MethodLength
77
+ raise ArgumentError, <<~MESSAGE unless HASH_VALUE_TRANSFORMS.include?(method)
78
+ invalid transform value (#{key}: #{value}).
79
+ Hash values are only valid for #{HASH_VALUE_TRANSFORMS}
80
+ MESSAGE
81
+
82
+ hash = hash.map do |k, v|
83
+ raise ArgumentError, <<~MESSAGE unless HASH_VALUE_KEYS.include?(k)
84
+ invalid transform value argument (#{method}: { #{k}: #{v} }).
85
+ Hash values are only valid for #{HASH_VALUE_TRANSFORMS}
86
+ MESSAGE
87
+
88
+ [k, v.to_sym]
89
+ end.to_h
90
+
91
+ ["#{method}_dynamic", hash]
92
+ end
93
+
94
+ RUBY_STRING_METHODS.each do |method|
95
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
96
+ def #{method}(string, _method_node)
97
+ string.#{method}
98
+ end
99
+ RUBY
100
+ end
101
+
102
+ # leftovers:call activesupport_available?
103
+ ACTIVESUPPORT_STRING_METHODS.each do |method|
104
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
105
+ def #{method}(string, _method_node)
106
+ return string unless activesupport_available?(:#{method})
107
+
108
+ string.#{method}
109
+ end
110
+ RUBY
111
+ end
112
+
113
+ def original(string, _method_node)
114
+ string
115
+ end
116
+
117
+ def delete_prefix(string, _method_node)
118
+ string.delete_prefix(@delete_prefix)
119
+ end
120
+
121
+ def delete_suffix(string, _method_node)
122
+ string.delete_suffix(@delete_suffix)
123
+ end
124
+
125
+ def delete_before(string, _method_node)
126
+ string.split(@delete_before, 2)[1] || string
127
+ end
128
+
129
+ def delete_after(string, _method_node)
130
+ string.split(@delete_after, 2).first
131
+ end
132
+
133
+ def replace_with(_string, _method_node)
134
+ @replace_with
135
+ end
136
+
137
+ def add_prefix(string, _method_node)
138
+ "#{@add_prefix}#{string}"
139
+ end
140
+
141
+ def add_suffix(string, _method_node)
142
+ "#{string}#{@add_suffix}"
143
+ end
144
+
145
+ def add_prefix_dynamic(string, method_node)
146
+ "#{dynamic_value(@add_prefix_dynamic, method_node)}#{@add_prefix_dynamic[:joiner]}#{string}"
147
+ end
148
+
149
+ def add_suffix_dynamic(string, method_node)
150
+ "#{string}#{@add_suffix_dynamic[:joiner]}#{dynamic_value(@add_suffix_dynamic, method_node)}"
151
+ end
152
+
153
+ def dynamic_value(value, method_node)
154
+ method_node.kwargs[value[:from_argument]].to_s if value[:from_argument]
155
+ end
156
+
157
+ def activesupport_available?(method) # rubocop:disable Metrics/MethodLength
158
+ Leftovers.try_require(
159
+ 'active_support/core_ext/string', 'active_support/inflections',
160
+ message: <<~MESSAGE
161
+ Tried transforming a string using an activesupport method (#{method}), but the activesupport gem was not available
162
+ `gem install activesupport`
163
+ MESSAGE
164
+ )
165
+
166
+ Leftovers.try_require(::File.join(Leftovers.pwd, 'config', 'initializers', 'inflections.rb'))
167
+
168
+ defined?(ActiveSupport)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leftovers
4
+ class ValueRule
5
+ def initialize(values) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
6
+ literal_values = Set.new
7
+ value_types = Set.new
8
+
9
+ Array.each_or_self(values) do |value|
10
+ case value
11
+ when Hash
12
+ raise ArgumentError, "invalid value #{value.inspect}" unless value[:type]
13
+
14
+ value_types.merge(
15
+ Array(value[:type]).map do |v|
16
+ case v
17
+ when 'String', :String then :str
18
+ when 'Symbol', :Symbol then :sym
19
+ else v.to_s.downcase.to_sym
20
+ end
21
+ end
22
+ )
23
+ else
24
+ (literal_values << value) if value
25
+ end
26
+ end
27
+
28
+ if literal_values.length <= 1
29
+ @literal_value = literal_values.first
30
+ @literal_values = nil
31
+ else
32
+ @literal_value = nil
33
+ @literal_values = literal_values
34
+ end
35
+
36
+ if value_types.length <= 1
37
+ @value_type = value_types.first
38
+ @value_types = nil
39
+ else
40
+ @value_type = nil
41
+ @value_types = value_types
42
+ end
43
+
44
+ freeze
45
+ end
46
+
47
+ def match?(value_node) # rubocop:disable Metrics/CyclomaticComplexity
48
+ return true if @value_type&.== value_node.type
49
+ return true if @value_types&.include?(value_node.type)
50
+ return false unless (@literal_value || @literal_values) && value_node.scalar?
51
+ return true if @literal_value&.== value_node.to_scalar_value
52
+
53
+ @literal_values&.include?(value_node.to_scalar_value)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leftovers
4
+ VERSION = '0.1.0'
5
+ end
data/lib/leftovers.rb ADDED
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './leftovers/core_ext'
4
+ require_relative './leftovers/backports'
5
+ require_relative './leftovers/collector'
6
+ require_relative './leftovers/merged_config'
7
+ require_relative './leftovers/reporter'
8
+
9
+ module Leftovers
10
+ module_function
11
+
12
+ class << self
13
+ attr_accessor :parallel
14
+ alias_method :parallel?, :parallel
15
+
16
+ attr_accessor :quiet
17
+ alias_method :quiet?, :quiet
18
+
19
+ attr_accessor :progress
20
+ alias_method :progress?, :progress
21
+ end
22
+
23
+ def stdout
24
+ @stdout ||= StringIO.new
25
+ end
26
+
27
+ def stderr
28
+ @stderr ||= StringIO.new
29
+ end
30
+
31
+ def config
32
+ @config ||= Leftovers::MergedConfig.new
33
+ end
34
+
35
+ def collector
36
+ @collector ||= Leftovers::Collector.new
37
+ end
38
+
39
+ def reporter
40
+ @reporter ||= Leftovers::Reporter.new
41
+ end
42
+
43
+ def leftovers # rubocop:disable Metrics/MethodLength
44
+ @leftovers ||= begin
45
+ collector.collect
46
+ collector.definitions.reject do |definition|
47
+ definition.skipped? || definition.in_collection?
48
+ end
49
+ end
50
+ end
51
+
52
+ def run(stdout: StringIO.new, stderr: StringIO.new) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
53
+ reset
54
+ @stdout = stdout
55
+ @stderr = stderr
56
+ return 0 if leftovers.empty?
57
+
58
+ only_test = []
59
+ none = []
60
+ leftovers.sort.each do |definition|
61
+ if !definition.test? && definition.in_test_collection?
62
+ only_test << definition
63
+ else
64
+ none << definition
65
+ end
66
+ end
67
+
68
+ unless only_test.empty?
69
+ puts "\e[31mOnly directly called in tests:\e[0m"
70
+ only_test.each { |definition| reporter.call(definition) }
71
+ end
72
+
73
+ unless none.empty?
74
+ puts "\e[31mNot directly called at all:\e[0m"
75
+ none.each { |definition| reporter.call(definition) }
76
+ end
77
+
78
+ 1
79
+ end
80
+
81
+ def reset # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
82
+ remove_instance_variable(:@config) if defined?(@config)
83
+ remove_instance_variable(:@collector) if defined?(@collector)
84
+ remove_instance_variable(:@reporter) if defined?(@reporter)
85
+ remove_instance_variable(:@leftovers) if defined?(@leftovers)
86
+ remove_instance_variable(:@try_require) if defined?(@try_require)
87
+ remove_instance_variable(:@stdout) if defined?(@stdout)
88
+ remove_instance_variable(:@stderr) if defined?(@stderr)
89
+ remove_instance_variable(:@parallel) if defined?(@parallel)
90
+ remove_instance_variable(:@quiet) if defined?(@quiet)
91
+ remove_instance_variable(:@pwd) if defined?(@pwd)
92
+ end
93
+
94
+ def warn(message)
95
+ stderr.puts("\e[2K#{message}") unless quiet?
96
+ end
97
+
98
+ def puts(message)
99
+ stdout.puts("\e[2K#{message}") unless quiet?
100
+ end
101
+
102
+ def print(message)
103
+ stdout.print(message) unless quiet?
104
+ end
105
+
106
+ def newline
107
+ stdout.puts('')
108
+ end
109
+
110
+ def pwd
111
+ @pwd ||= Pathname.new(Dir.pwd + '/')
112
+ end
113
+
114
+ def try_require(*requirables, message: nil) # rubocop:disable Metrics/MethodLength
115
+ @try_require ||= {}
116
+ requirables.each do |requirable|
117
+ begin
118
+ return @try_require[requirable] if @try_require.key?(requirable)
119
+
120
+ @try_require[requirable] = require requirable
121
+ rescue LoadError
122
+ warn message if message
123
+ @try_require[requirable] = false
124
+ end
125
+ end
126
+ end
127
+ end