fastererer 0.12.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.
- checksums.yaml +7 -0
- data/.fastererer.yml +27 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +113 -0
- data/CODE_OF_CONDUCT.md +18 -0
- data/CONTRIBUTING.md +63 -0
- data/LICENSE.txt +23 -0
- data/README.md +169 -0
- data/Rakefile +12 -0
- data/SECURITY.md +20 -0
- data/exe/fastererer +7 -0
- data/lib/fastererer/analyzer.rb +94 -0
- data/lib/fastererer/cli.rb +43 -0
- data/lib/fastererer/config.rb +50 -0
- data/lib/fastererer/file_traverser.rb +186 -0
- data/lib/fastererer/method_call.rb +141 -0
- data/lib/fastererer/method_definition.rb +94 -0
- data/lib/fastererer/offense.rb +81 -0
- data/lib/fastererer/offense_collector.rb +19 -0
- data/lib/fastererer/painter.rb +45 -0
- data/lib/fastererer/parser.rb +13 -0
- data/lib/fastererer/rescue_call.rb +23 -0
- data/lib/fastererer/scanners/method_call_scanner.rb +139 -0
- data/lib/fastererer/scanners/method_definition_scanner.rb +94 -0
- data/lib/fastererer/scanners/offensive.rb +25 -0
- data/lib/fastererer/scanners/rescue_call_scanner.rb +30 -0
- data/lib/fastererer/scanners/symbol_to_proc_check.rb +35 -0
- data/lib/fastererer/version.rb +5 -0
- data/lib/fastererer.rb +9 -0
- metadata +92 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'English'
|
|
5
|
+
|
|
6
|
+
require_relative 'analyzer'
|
|
7
|
+
require_relative 'config'
|
|
8
|
+
require_relative 'painter'
|
|
9
|
+
|
|
10
|
+
module Fastererer
|
|
11
|
+
class FileTraverser
|
|
12
|
+
CONFIG_FILE_NAME = Config::FILE_NAME
|
|
13
|
+
SPEEDUPS_KEY = Config::SPEEDUPS_KEY
|
|
14
|
+
EXCLUDE_PATHS_KEY = Config::EXCLUDE_PATHS_KEY
|
|
15
|
+
|
|
16
|
+
attr_reader :config, :parse_error_paths
|
|
17
|
+
attr_accessor :offenses_total_count
|
|
18
|
+
|
|
19
|
+
def initialize(path)
|
|
20
|
+
@path = Pathname(path || '.')
|
|
21
|
+
@parse_error_paths = []
|
|
22
|
+
@config = Config.new
|
|
23
|
+
@offenses_total_count = 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def traverse
|
|
27
|
+
traverse_files
|
|
28
|
+
output_parse_errors
|
|
29
|
+
output_statistics
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def config_file
|
|
33
|
+
config.file
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def offenses_found?
|
|
37
|
+
!!offenses_found
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def scannable_files
|
|
41
|
+
all_files - ignored_files
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_accessor :offenses_found
|
|
47
|
+
|
|
48
|
+
def traverse_files
|
|
49
|
+
if @path.exist?
|
|
50
|
+
scannable_files.each { |ruby_file| scan_file(ruby_file) }
|
|
51
|
+
else
|
|
52
|
+
output_unable_to_find_file(@path)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scan_file(path)
|
|
57
|
+
analyzer = Analyzer.new(path)
|
|
58
|
+
analyzer.scan
|
|
59
|
+
rescue RubyParser::SyntaxError, Racc::ParseError, Timeout::Error => e
|
|
60
|
+
parse_error_paths.push(ErrorData.new(path, e.class, e.message).to_s)
|
|
61
|
+
else
|
|
62
|
+
if offenses_grouped_by_type(analyzer).any?
|
|
63
|
+
output(analyzer)
|
|
64
|
+
self.offenses_found = true
|
|
65
|
+
self.offenses_total_count += analyzer.errors.count
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def all_files
|
|
70
|
+
if @path.directory?
|
|
71
|
+
Dir[File.join(@path, '**', '*.rb')].map do |ruby_file_path|
|
|
72
|
+
Pathname(ruby_file_path).relative_path_from(root_dir).to_s
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
[@path.to_s]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def root_dir
|
|
80
|
+
@root_dir ||= Pathname('.')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def output(analyzer)
|
|
84
|
+
offenses_grouped_by_type(analyzer).each do |error_group_name, error_occurences|
|
|
85
|
+
error_occurences.map(&:line_number).each do |line|
|
|
86
|
+
file_and_line = "#{analyzer.file_path}:#{line}"
|
|
87
|
+
print "#{Painter.paint(file_and_line, :red)} #{Fastererer::Offense::EXPLANATIONS[error_group_name]}.\n"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
print "\n"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def offenses_grouped_by_type(analyzer)
|
|
95
|
+
analyzer.errors.group_by(&:name).delete_if do |offense_name, _|
|
|
96
|
+
ignored_speedups.include?(offense_name)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def output_parse_errors
|
|
101
|
+
return if parse_error_paths.none?
|
|
102
|
+
|
|
103
|
+
puts 'Fastererer was unable to process some files because the'
|
|
104
|
+
puts 'internal parser is not able to read some characters or'
|
|
105
|
+
puts 'has timed out. Unprocessable files were:'
|
|
106
|
+
puts '-----------------------------------------------------'
|
|
107
|
+
puts parse_error_paths
|
|
108
|
+
puts
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def output_statistics
|
|
112
|
+
puts Statistics.new(self)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def output_unable_to_find_file(path)
|
|
116
|
+
puts Painter.paint("No such file or directory - #{path}", :red)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ignored_speedups
|
|
120
|
+
config.ignored_speedups
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ignored_files
|
|
124
|
+
config.ignored_files
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def nil_config_file
|
|
128
|
+
config.nil_file
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
ErrorData = Struct.new(:file_path, :error_class, :error_message) do
|
|
133
|
+
def to_s
|
|
134
|
+
"#{file_path} - #{error_class} - #{error_message}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class Statistics
|
|
139
|
+
def initialize(traverser)
|
|
140
|
+
@files_inspected_count = traverser.scannable_files.count
|
|
141
|
+
@offenses_found_count = traverser.offenses_total_count
|
|
142
|
+
@unparsable_files_count = traverser.parse_error_paths.count
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def to_s
|
|
146
|
+
[
|
|
147
|
+
inspected_files_output,
|
|
148
|
+
offenses_found_output,
|
|
149
|
+
unparsable_files_output
|
|
150
|
+
].compact.join(', ')
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def inspected_files_output
|
|
154
|
+
Painter.paint(
|
|
155
|
+
"#{@files_inspected_count} #{pluralize(@files_inspected_count, 'file')} inspected", :green
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def offenses_found_output
|
|
160
|
+
color = @offenses_found_count.zero? ? :green : :red
|
|
161
|
+
|
|
162
|
+
Painter.paint(
|
|
163
|
+
"#{@offenses_found_count} #{pluralize(@offenses_found_count, 'offense')} detected", color
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def unparsable_files_output
|
|
168
|
+
return if @unparsable_files_count.zero?
|
|
169
|
+
|
|
170
|
+
Painter.paint(
|
|
171
|
+
"#{@unparsable_files_count} unparsable #{pluralize(@unparsable_files_count, 'file')} found",
|
|
172
|
+
:red
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def pluralize(count, singular, plural = nil)
|
|
177
|
+
if count == 1
|
|
178
|
+
singular.to_s
|
|
179
|
+
elsif plural
|
|
180
|
+
plural.to_s
|
|
181
|
+
else
|
|
182
|
+
"#{singular}s"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
class MethodCall
|
|
5
|
+
attr_reader :element, :receiver, :method_name, :arguments, :block_body, :block_argument_names
|
|
6
|
+
|
|
7
|
+
alias name method_name
|
|
8
|
+
|
|
9
|
+
def initialize(element)
|
|
10
|
+
@element = element
|
|
11
|
+
set_call_element
|
|
12
|
+
set_receiver
|
|
13
|
+
set_method_name
|
|
14
|
+
set_arguments
|
|
15
|
+
set_block_presence
|
|
16
|
+
set_block_body
|
|
17
|
+
set_block_argument_names
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def block?
|
|
21
|
+
@block_present || false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def receiver_element
|
|
25
|
+
call_element[1]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def arguments_element
|
|
29
|
+
call_element.sexp_body(3) || []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def lambda_literal?
|
|
33
|
+
call_element.sexp_type == :lambda
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :call_element
|
|
39
|
+
|
|
40
|
+
# TODO: explanation
|
|
41
|
+
def set_call_element
|
|
42
|
+
@call_element = case element.sexp_type
|
|
43
|
+
when :call
|
|
44
|
+
@element
|
|
45
|
+
when :iter
|
|
46
|
+
@element[1]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_receiver
|
|
51
|
+
@receiver = ReceiverFactory.new(receiver_element)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def set_method_name
|
|
55
|
+
@method_name = call_element[2]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def set_arguments
|
|
59
|
+
@arguments = arguments_element.map { |argument| Argument.new(argument) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_block_presence
|
|
63
|
+
if element.sexp_type == :iter || (arguments.last && arguments.last.type == :block_pass)
|
|
64
|
+
@block_present = true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def set_block_body
|
|
69
|
+
@block_body = element[3] if block?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# TODO: write specs for lambdas and procs
|
|
73
|
+
def set_block_argument_names
|
|
74
|
+
@block_argument_names = if block? && element[2].is_a?(Sexp) # HACK: for lambdas
|
|
75
|
+
element[2].drop(1).map { |argument| argument }
|
|
76
|
+
end || []
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# For now, used for determening if the
|
|
81
|
+
# receiver is a reference or a method call.
|
|
82
|
+
class ReceiverFactory
|
|
83
|
+
def self.new(receiver_info)
|
|
84
|
+
return unless receiver_info.is_a?(Sexp)
|
|
85
|
+
|
|
86
|
+
case receiver_info.sexp_type
|
|
87
|
+
when :lvar
|
|
88
|
+
VariableReference.new(receiver_info)
|
|
89
|
+
when :call, :iter
|
|
90
|
+
MethodCall.new(receiver_info)
|
|
91
|
+
when :array, :dot2, :dot3, :lit
|
|
92
|
+
Primitive.new(receiver_info)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class VariableReference
|
|
98
|
+
attr_reader :name
|
|
99
|
+
|
|
100
|
+
def initialize(reference_info)
|
|
101
|
+
@reference_info = reference_info
|
|
102
|
+
@name = reference_info[1]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class Argument
|
|
107
|
+
attr_reader :element
|
|
108
|
+
|
|
109
|
+
def initialize(element)
|
|
110
|
+
@element = element
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def type
|
|
114
|
+
@type ||= @element[0]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def value
|
|
118
|
+
@value ||= @element[1]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class Primitive
|
|
123
|
+
attr_reader :element
|
|
124
|
+
|
|
125
|
+
def initialize(element)
|
|
126
|
+
@element = element
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def type
|
|
130
|
+
@type ||= @element[0]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def range?
|
|
134
|
+
%i[dot2 dot3 lit].include?(type)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def array?
|
|
138
|
+
type == :array
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
class MethodDefinition
|
|
5
|
+
# Exposed for testing purposes.
|
|
6
|
+
attr_reader :element, :method_name, :block_argument_name, :body, :arguments
|
|
7
|
+
|
|
8
|
+
alias name method_name
|
|
9
|
+
|
|
10
|
+
def initialize(element)
|
|
11
|
+
@element = element # Ripper element
|
|
12
|
+
set_method_name
|
|
13
|
+
set_body
|
|
14
|
+
set_arguments
|
|
15
|
+
set_block_argument_name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def block?
|
|
19
|
+
!!@block_argument_name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def setter?
|
|
23
|
+
name.to_s.end_with?('=')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def arguments_element
|
|
29
|
+
element[2].drop(1) || []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def set_method_name
|
|
33
|
+
@method_name = @element[1]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def set_arguments
|
|
37
|
+
@arguments = arguments_element.map do |argument_element|
|
|
38
|
+
MethodDefinitionArgument.new(argument_element)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_body
|
|
43
|
+
@body = @element[3..]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def set_block_argument_name
|
|
47
|
+
return unless last_argument_element.to_s.start_with?('&')
|
|
48
|
+
|
|
49
|
+
@block_argument_name = last_argument_element.to_s.delete_prefix('&').to_sym
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def last_argument_element
|
|
53
|
+
arguments_element.last
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class MethodDefinitionArgument
|
|
58
|
+
attr_reader :element, :name, :type
|
|
59
|
+
|
|
60
|
+
def initialize(element)
|
|
61
|
+
@element = element
|
|
62
|
+
set_name
|
|
63
|
+
set_argument_type
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def regular_argument?
|
|
67
|
+
@type == :regular_argument
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def default_argument?
|
|
71
|
+
@type == :default_argument
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def keyword_argument?
|
|
75
|
+
@type == :keyword_argument
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def set_name
|
|
81
|
+
@name = element.is_a?(Symbol) ? element : element[1]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def set_argument_type
|
|
85
|
+
@type = if element.is_a?(Symbol)
|
|
86
|
+
:regular_argument
|
|
87
|
+
elsif element.is_a?(Sexp) && element.sexp_type == :lasgn
|
|
88
|
+
:default_argument
|
|
89
|
+
elsif element.is_a?(Sexp) && element.sexp_type == :kwarg
|
|
90
|
+
:keyword_argument
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
class Offense
|
|
5
|
+
attr_reader :offense_name, :line_number
|
|
6
|
+
|
|
7
|
+
alias name offense_name
|
|
8
|
+
alias line line_number
|
|
9
|
+
|
|
10
|
+
def initialize(offense_name, line_number)
|
|
11
|
+
@offense_name = offense_name
|
|
12
|
+
@line_number = line_number
|
|
13
|
+
explanation # Set explanation right away.
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def explanation
|
|
17
|
+
@explanation ||= EXPLANATIONS.fetch(offense_name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
EXPLANATIONS = {
|
|
21
|
+
rescue_vs_respond_to:
|
|
22
|
+
'Don\'t rescue NoMethodError, rather check with respond_to?',
|
|
23
|
+
|
|
24
|
+
module_eval:
|
|
25
|
+
'Using module_eval is slower than define_method',
|
|
26
|
+
|
|
27
|
+
shuffle_first_vs_sample:
|
|
28
|
+
'Array#shuffle.first is slower than Array#sample',
|
|
29
|
+
|
|
30
|
+
for_loop_vs_each:
|
|
31
|
+
'For loop is slower than using each',
|
|
32
|
+
|
|
33
|
+
each_with_index_vs_while:
|
|
34
|
+
'Using each_with_index is slower than while loop',
|
|
35
|
+
|
|
36
|
+
map_flatten_vs_flat_map:
|
|
37
|
+
'Array#map.flatten(1) is slower than Array#flat_map',
|
|
38
|
+
|
|
39
|
+
reverse_each_vs_reverse_each:
|
|
40
|
+
'Array#reverse.each is slower than Array#reverse_each',
|
|
41
|
+
|
|
42
|
+
select_first_vs_detect:
|
|
43
|
+
'Array#select.first is slower than Array#detect',
|
|
44
|
+
|
|
45
|
+
sort_vs_sort_by:
|
|
46
|
+
'Enumerable#sort is slower than Enumerable#sort_by',
|
|
47
|
+
|
|
48
|
+
fetch_with_argument_vs_block:
|
|
49
|
+
'Hash#fetch with second argument is slower than Hash#fetch with block',
|
|
50
|
+
|
|
51
|
+
keys_each_vs_each_key:
|
|
52
|
+
'Hash#keys.each is slower than Hash#each_key. N.B. Hash#each_key cannot be used if ' \
|
|
53
|
+
'the hash is modified during the each block',
|
|
54
|
+
|
|
55
|
+
hash_merge_bang_vs_hash_brackets:
|
|
56
|
+
'Hash#merge! with one argument is slower than Hash#[]',
|
|
57
|
+
|
|
58
|
+
block_vs_symbol_to_proc:
|
|
59
|
+
'Calling argumentless methods within blocks is slower than using symbol to proc',
|
|
60
|
+
|
|
61
|
+
proc_call_vs_yield:
|
|
62
|
+
'Calling blocks with call is slower than yielding',
|
|
63
|
+
|
|
64
|
+
gsub_vs_tr:
|
|
65
|
+
'Using tr is faster than gsub when replacing a single character in a string with ' \
|
|
66
|
+
'another single character',
|
|
67
|
+
|
|
68
|
+
select_last_vs_reverse_detect:
|
|
69
|
+
'Array#select.last is slower than Array#reverse.detect',
|
|
70
|
+
|
|
71
|
+
getter_vs_attr_reader:
|
|
72
|
+
'Use attr_reader for reading ivars',
|
|
73
|
+
|
|
74
|
+
setter_vs_attr_writer:
|
|
75
|
+
'Use attr_writer for writing to ivars',
|
|
76
|
+
|
|
77
|
+
include_vs_cover_on_range:
|
|
78
|
+
'Use #cover? instead of #include? on ranges'
|
|
79
|
+
}.freeze
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
module Fastererer
|
|
6
|
+
class OffenseCollector
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@offenses = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def [](offense_name)
|
|
14
|
+
@offenses.select { |offense| offense.name == offense_name }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def_delegators :@offenses, :push, :any?, :each, :group_by, :count
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
module Painter
|
|
5
|
+
COLOR_CODES = {
|
|
6
|
+
red: 31,
|
|
7
|
+
green: 32
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
@disabled = false
|
|
11
|
+
|
|
12
|
+
def self.paint(string, color)
|
|
13
|
+
# Validate before short-circuit so bad color symbols surface even with --no-color/NO_COLOR.
|
|
14
|
+
color_code = COLOR_CODES[color.to_sym]
|
|
15
|
+
if color_code.nil?
|
|
16
|
+
raise ArgumentError,
|
|
17
|
+
"Color #{color} is not supported. Allowed colors are #{COLOR_CODES.keys.join(', ')}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
return string unless colorize?
|
|
21
|
+
|
|
22
|
+
paint_with_code(string, color_code)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.paint_with_code(string, color_code)
|
|
26
|
+
"\e[#{color_code}m#{string}\e[0m"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.disable!
|
|
30
|
+
@disabled = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Re-enables colorization; production never calls this — exists so tests can reset state.
|
|
34
|
+
def self.enable!
|
|
35
|
+
@disabled = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.colorize?
|
|
39
|
+
return false unless ENV.fetch('NO_COLOR', '').empty?
|
|
40
|
+
return false if @disabled
|
|
41
|
+
|
|
42
|
+
$stdout.tty?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
class RescueCall
|
|
5
|
+
attr_reader :element, :rescue_classes
|
|
6
|
+
|
|
7
|
+
def initialize(element)
|
|
8
|
+
@element = element
|
|
9
|
+
@rescue_classes = []
|
|
10
|
+
set_rescue_classes
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def set_rescue_classes
|
|
16
|
+
return if element[1].sexp_type != :array
|
|
17
|
+
|
|
18
|
+
@rescue_classes = element[1].drop(1).filter_map do |rescue_reference|
|
|
19
|
+
rescue_reference[1] if rescue_reference.sexp_type == :const
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|