ruby_friendly_error 0.0.1 → 0.0.2
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 +4 -4
- data/.gitignore +2 -0
- data/.rubocop_todo.yml +2 -2
- data/Gemfile.lock +2 -2
- data/README.adoc +65 -1
- data/exe/ruby_friendly_error +1 -1
- data/lib/ruby_friendly_error/ast.rb +53 -1
- data/lib/ruby_friendly_error/renderers/argument_error_renderer.rb +37 -0
- data/lib/ruby_friendly_error/renderers/exception_renderer.rb +136 -0
- data/lib/ruby_friendly_error/renderers/missing_end_error_renderer.rb +15 -0
- data/lib/ruby_friendly_error/renderers/name_error_renderer.rb +30 -0
- data/lib/ruby_friendly_error/renderers/standard_error_renderer.rb +27 -0
- data/lib/ruby_friendly_error/renderers/syntax_error_renderer.rb +23 -0
- data/lib/ruby_friendly_error/renderers/unnecessary_end_error_renderer.rb +15 -0
- data/lib/ruby_friendly_error/utils.rb +23 -0
- data/lib/ruby_friendly_error/version.rb +1 -1
- data/lib/ruby_friendly_error.rb +38 -112
- data/locales/en.yml +13 -5
- data/locales/ja.yml +22 -0
- data/ruby_friendly_error.gemspec +4 -4
- data/samples/arg_error.rb +8 -0
- data/samples/miss_spell1.rb +3 -14
- data/samples/not_error.rb +2 -0
- metadata +16 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f4b1bac560baef1e3575b2d65a0d0cd90c40f3daba2219b2d0b6dc6eef9b06e
|
4
|
+
data.tar.gz: d3baae55366188eb7df5d5cd9bbdadfc8273ac90e821cb31aa85404867277668
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b47bafe16356f7ae7c875350f81c756a3f28cce4d7e58fdf0a7002b0a6688e7fc1824d562f5229fcf35f87441b083f1e18640a218d1ede235b2753ac30bc0df3
|
7
|
+
data.tar.gz: d5db46fa860dcaf072d0a9d726088222ce945e7ae10b6c26085d2ac62f94353df8a2a559361997997cd84274e1bf7f03ccdebc644c58ff5978cf074cc772b99b
|
data/.gitignore
CHANGED
data/.rubocop_todo.yml
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
Metrics/
|
1
|
+
Metrics/ClassLength:
|
2
2
|
Exclude:
|
3
|
-
- lib/ruby_friendly_error.rb
|
3
|
+
- lib/ruby_friendly_error/renderers/exception_renderer.rb
|
data/Gemfile.lock
CHANGED
data/README.adoc
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
:sectnums:
|
7
7
|
:source-highlighter: highlightjs
|
8
8
|
:toc: left
|
9
|
-
:toclevels:
|
9
|
+
:toclevels: 2
|
10
10
|
|
11
11
|
= RubyFriendlyError image:https://img.shields.io/badge/ruby-2.4.4-cc342d.svg["ruby 2.4.4", link="https://www.ruby-lang.org/en/news/2018/03/28/ruby-2-4-4-released/"] image:https://img.shields.io/badge/ruby-2.5.1-cc342d.svg["ruby 2.5.1", link="https://www.ruby-lang.org/en/news/2018/03/28/ruby-2-5-1-released/"]
|
12
12
|
|
@@ -24,6 +24,70 @@ gem 'ruby_friendly_error'
|
|
24
24
|
$ bundle exec ruby_friendly_error your.rb
|
25
25
|
```
|
26
26
|
|
27
|
+
== Samples
|
28
|
+
|
29
|
+
=== miss spell
|
30
|
+
|
31
|
+
[source,ruby]
|
32
|
+
.sample.rb
|
33
|
+
----
|
34
|
+
# frozen_string_literal: true
|
35
|
+
|
36
|
+
def hoge prayer_life = 100 , player_lifee = 200
|
37
|
+
puts 'hoge' if player_life > 0
|
38
|
+
end
|
39
|
+
|
40
|
+
hoge
|
41
|
+
----
|
42
|
+
|
43
|
+
```sh
|
44
|
+
$ bundle exec ruby_friendly_error sample.rb
|
45
|
+
```
|
46
|
+
|
47
|
+
image:https://raw.githubusercontent.com/isuke/ruby_friendly_error/images/name_error_with_did_you_mean.png["name_error_with_did_you_mean", caption="output"]
|
48
|
+
|
49
|
+
=== miss args num
|
50
|
+
|
51
|
+
[source,ruby]
|
52
|
+
.sample.rb
|
53
|
+
----
|
54
|
+
# frozen_string_literal: true
|
55
|
+
|
56
|
+
def hoge arg1, arg2 = 'foobar'
|
57
|
+
puts arg1
|
58
|
+
puts arg2
|
59
|
+
end
|
60
|
+
|
61
|
+
hoge 'piyo', 'fuga', 'what!?'
|
62
|
+
----
|
63
|
+
|
64
|
+
```sh
|
65
|
+
$ bundle exec ruby_friendly_error sample.rb
|
66
|
+
```
|
67
|
+
|
68
|
+
image:https://raw.githubusercontent.com/isuke/ruby_friendly_error/images/wrong_number_of_arguments_error.png["wrong_number_of_arguments_error", caption="output"]
|
69
|
+
|
70
|
+
== Options
|
71
|
+
|
72
|
+
[cols="1,1,1", options="header"]
|
73
|
+
|===
|
74
|
+
| key
|
75
|
+
| description
|
76
|
+
| values
|
77
|
+
|
78
|
+
| RUBY_FRIENDLY_ERROR_LANG
|
79
|
+
| message language.
|
80
|
+
| en, ja
|
81
|
+
|===
|
82
|
+
|
83
|
+
=== RUBY_FRIENDLY_ERROR_LANG
|
84
|
+
|
85
|
+
```sh
|
86
|
+
$ RUBY_FRIENDLY_ERROR_LANG=ja bundle exec ruby_friendly_error sample.rb
|
87
|
+
```
|
88
|
+
|
89
|
+
image:https://raw.githubusercontent.com/isuke/ruby_friendly_error/images/wrong_number_of_arguments_error_ja.png["wrong_number_of_arguments_error_ja", caption="output"]
|
90
|
+
|
27
91
|
== Development
|
28
92
|
|
29
93
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/ruby_friendly_error
CHANGED
@@ -35,6 +35,20 @@ class Parser::AST::Node
|
|
35
35
|
KEYWORD_REST_ARG_TYPE,
|
36
36
|
].freeze
|
37
37
|
|
38
|
+
REQURED_ARG_TYPES = [
|
39
|
+
ARG_TYPE,
|
40
|
+
].freeze
|
41
|
+
|
42
|
+
OPTIONAL_ARG_TYPES = [
|
43
|
+
OPTION_ARG_TYPE,
|
44
|
+
KEYWORD_ARG_OPTION_TYPE,
|
45
|
+
].freeze
|
46
|
+
|
47
|
+
DEF_TYPE = :def
|
48
|
+
SEND_TYPE = :send
|
49
|
+
|
50
|
+
NOT_HAVE_LINE_TYPES = %i[args].freeze
|
51
|
+
|
38
52
|
def find_by_variable_name variable_name
|
39
53
|
to_a.each do |node|
|
40
54
|
next unless node.is_a? Parser::AST::Node
|
@@ -42,9 +56,47 @@ class Parser::AST::Node
|
|
42
56
|
return node if node.to_a[0].to_s.sub(/^@*/, '') == variable_name.to_s
|
43
57
|
end
|
44
58
|
|
45
|
-
result = node.find_by_variable_name
|
59
|
+
result = node.find_by_variable_name variable_name
|
60
|
+
return result if result
|
61
|
+
end
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_by_line_number line_number
|
66
|
+
to_a.each do |node|
|
67
|
+
next unless node.is_a? Parser::AST::Node
|
68
|
+
next if NOT_HAVE_LINE_TYPES.include? node.type
|
69
|
+
return node if node.location.line == line_number
|
70
|
+
result = node.find_by_line_number line_number
|
71
|
+
return result if result
|
72
|
+
end
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_send_by_def_name def_name
|
77
|
+
to_a.each do |node|
|
78
|
+
next unless node.is_a? Parser::AST::Node
|
79
|
+
|
80
|
+
return node if node.type == SEND_TYPE && node.to_a[1].to_s == def_name.to_s
|
81
|
+
|
82
|
+
result = node.find_send_by_def_name def_name
|
46
83
|
return result if result
|
47
84
|
end
|
48
85
|
nil
|
49
86
|
end
|
87
|
+
|
88
|
+
def def_args_num_range
|
89
|
+
return nil unless type == DEF_TYPE
|
90
|
+
|
91
|
+
required_arg_num = to_a[1].to_a.select { |n| REQURED_ARG_TYPES.include? n.type }.size
|
92
|
+
optionarl_arg_num = to_a[1].to_a.select { |n| OPTIONAL_ARG_TYPES.include? n.type }.size
|
93
|
+
|
94
|
+
(required_arg_num..(required_arg_num + optionarl_arg_num))
|
95
|
+
end
|
96
|
+
|
97
|
+
def send_args_num
|
98
|
+
return nil unless type == SEND_TYPE
|
99
|
+
|
100
|
+
to_a.size - 2
|
101
|
+
end
|
50
102
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError::Renderers
|
4
|
+
class ArgumentErrorRenderer < StandardErrorRenderer
|
5
|
+
private
|
6
|
+
|
7
|
+
def exception_i18n_name
|
8
|
+
I18n.t 'standard_error.argument_error.title'
|
9
|
+
end
|
10
|
+
|
11
|
+
def def_node
|
12
|
+
@def_node ||= ast.find_by_line_number error_line_number
|
13
|
+
end
|
14
|
+
|
15
|
+
def def_name
|
16
|
+
@def_name ||= def_node.to_a[0]
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_node
|
20
|
+
@send_node ||= ast.find_send_by_def_name def_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def error_message
|
24
|
+
args_num_range = def_node.def_args_num_range
|
25
|
+
|
26
|
+
args_num_str = args_num_range.size == 1 ? args_num_range.first.to_s : args_num_range.to_s
|
27
|
+
|
28
|
+
I18n.t 'standard_error.argument_error.wrong_number_of_arguments', def_name: def_name, arg_range: args_num_str, send_arg: send_node.send_args_num
|
29
|
+
end
|
30
|
+
|
31
|
+
def display_error_detail
|
32
|
+
display_error_line_string def_node.location.line, strong_pos_range: ((def_node.location.column + 1)..def_node.to_a[1].location.last_column)
|
33
|
+
|
34
|
+
display_error_line_string send_node.location.line, strong_pos_range: ((send_node.location.column + 1)..send_node.location.last_column)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError::Renderers
|
4
|
+
class ExceptionRenderer
|
5
|
+
attr_reader :exception
|
6
|
+
attr_reader :eval_file_content
|
7
|
+
|
8
|
+
def initialize exception, eval_file_content
|
9
|
+
@exception = exception
|
10
|
+
@eval_file_content = eval_file_content
|
11
|
+
end
|
12
|
+
|
13
|
+
def render
|
14
|
+
display_backtrace exception.backtrace unless eval?
|
15
|
+
|
16
|
+
display_main_message
|
17
|
+
|
18
|
+
display_error_detail
|
19
|
+
|
20
|
+
display_message
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def file_name
|
26
|
+
@file_name ||= File.basename(file_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def file_path
|
30
|
+
raise NotImplementedError, "You must implement #{__method__}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def file_expand_path
|
34
|
+
@file_expand_path ||= File.expand_path(file_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def file_content
|
38
|
+
raise NotImplementedError, "You must implement #{__method__}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def eval?
|
42
|
+
file_name == '(eval)'
|
43
|
+
end
|
44
|
+
|
45
|
+
def exception_i18n_name
|
46
|
+
raise NotImplementedError, "You must implement #{__method__}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def error_line_number
|
50
|
+
raise NotImplementedError, "You must implement #{__method__}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def error_message
|
54
|
+
raise NotImplementedError, "You must implement #{__method__}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def display_backtrace backtrace
|
58
|
+
fake_backtrace = backtrace
|
59
|
+
.dup
|
60
|
+
.delete_if { |b| b.match(/ruby_friendly_error/) }
|
61
|
+
.delete_if { |b| b.match(/`block in exec'/) }
|
62
|
+
.map do |b|
|
63
|
+
if b.match? Regexp.new("#{@file_name}.*`exec'")
|
64
|
+
b.sub("`exec'", "`<main>'")
|
65
|
+
else
|
66
|
+
b
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
fake_backtrace_size = fake_backtrace.size
|
71
|
+
fake_backtrace_digits = fake_backtrace_size.to_s.length
|
72
|
+
|
73
|
+
fake_backtrace.reverse_each.with_index do |b, i|
|
74
|
+
num = fake_backtrace_size - i
|
75
|
+
num_digits = num.to_s.length
|
76
|
+
color = b.include?(@file_name) ? :light_red : :light_yellow
|
77
|
+
STDERR.puts "#{' ' * (fake_backtrace_digits - num_digits)}#{num}: #{b}".colorize(color)
|
78
|
+
end
|
79
|
+
STDERR.puts
|
80
|
+
end
|
81
|
+
|
82
|
+
def display_main_message
|
83
|
+
STDERR.puts "#{exception_i18n_name.underline}#{I18n.t('main_message')}: #{file_path}:#{error_line_number}".colorize(:light_red)
|
84
|
+
STDERR.puts
|
85
|
+
end
|
86
|
+
|
87
|
+
def display_error_detail
|
88
|
+
raise NotImplementedError, "You must implement #{__method__}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def display_message
|
92
|
+
STDERR.puts "#{exception_i18n_name.underline}:".colorize(:light_red)
|
93
|
+
STDERR.puts format_line_strings(error_message) { |l, _i| " #{l}" }.colorize(:light_red)
|
94
|
+
end
|
95
|
+
|
96
|
+
def display_error_line_string line_number = error_line_number, strong_pos_range: nil, window: RubyFriendlyError::WINDOW
|
97
|
+
line_strings = file_content.split("\n", -1)
|
98
|
+
file_line_size = line_strings.size
|
99
|
+
window_start = [line_number - window, 1].max
|
100
|
+
window_end = [line_number + window, file_line_size].min
|
101
|
+
|
102
|
+
STDERR.puts RubyFriendlyError::DISPLAY_START.colorize(:light_yellow)
|
103
|
+
|
104
|
+
display_error_line_string_window line_strings, window_start, line_number - 1 if line_number > 1
|
105
|
+
|
106
|
+
lint_string =
|
107
|
+
if strong_pos_range
|
108
|
+
RubyFriendlyError::Utils.replace_string_at(line_strings[line_number - 1], strong_pos_range, &:underline)
|
109
|
+
else
|
110
|
+
line_strings[line_number - 1]
|
111
|
+
end
|
112
|
+
STDERR.puts "#{line_number}: #{lint_string}".rstrip.colorize(:light_red)
|
113
|
+
|
114
|
+
display_error_line_string_window line_strings, line_number + 1, window_end if line_number < file_line_size
|
115
|
+
|
116
|
+
STDERR.puts RubyFriendlyError::DISPLAY_END.colorize(:light_yellow)
|
117
|
+
STDERR.puts
|
118
|
+
end
|
119
|
+
|
120
|
+
def display_error_line_string_window line_strings, start_line_number, end_line_number
|
121
|
+
STDERR.puts format_line_strings(line_strings, start_line_number, end_line_number) { |l, i| "#{start_line_number + i}: #{l}" }&.colorize(:light_yellow)
|
122
|
+
end
|
123
|
+
|
124
|
+
def format_line_strings line_strings, start_line = 1, end_line = 0
|
125
|
+
line_strings =
|
126
|
+
case line_strings
|
127
|
+
when String then line_strings.split("\n", -1)
|
128
|
+
when Array then line_strings
|
129
|
+
end
|
130
|
+
line_strings[(start_line - 1)..(end_line - 1)]
|
131
|
+
&.map
|
132
|
+
&.with_index { |l, i| block_given? ? yield(l, i).rstrip : l.rstrip }
|
133
|
+
&.join("\n")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError::Renderers
|
4
|
+
class MissingEndErrorRenderer < SyntaxErrorRenderer
|
5
|
+
private
|
6
|
+
|
7
|
+
def error_message
|
8
|
+
I18n.t('syntax_error.missing_end')
|
9
|
+
end
|
10
|
+
|
11
|
+
def display_error_detail
|
12
|
+
display_error_line_string
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError::Renderers
|
4
|
+
class NameEndErrorRenderer < StandardErrorRenderer
|
5
|
+
private
|
6
|
+
|
7
|
+
def exception_i18n_name
|
8
|
+
I18n.t 'standard_error.name_error.title'
|
9
|
+
end
|
10
|
+
|
11
|
+
def corrections
|
12
|
+
@corrections ||= exception.spell_checker.corrections
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_message
|
16
|
+
var_name_str = exception.spell_checker.name.underline
|
17
|
+
corrections_str = corrections.map { |c| "`#{c.to_s.underline}`" }.join(', ')
|
18
|
+
I18n.t('standard_error.name_error.with_did_you_mean', var_name: var_name_str, corrections: corrections_str)
|
19
|
+
end
|
20
|
+
|
21
|
+
def display_error_detail
|
22
|
+
corrections.each do |var_name|
|
23
|
+
node_location = ast.find_by_variable_name(var_name).location
|
24
|
+
display_error_line_string node_location.line, strong_pos_range: ((node_location.column + 1)..node_location.last_column)
|
25
|
+
end
|
26
|
+
|
27
|
+
display_error_line_string strong_pos_range: ((error_node.location.column + 1)..error_node.location.last_column)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError::Renderers
|
4
|
+
class StandardErrorRenderer < ExceptionRenderer
|
5
|
+
private
|
6
|
+
|
7
|
+
def ast
|
8
|
+
@ast ||= RubyFriendlyError::Utils.suppress_error_display { Parser::CurrentRuby.parse file_content }
|
9
|
+
end
|
10
|
+
|
11
|
+
def error_node
|
12
|
+
@error_node ||= ast.find_by_line_number error_line_number
|
13
|
+
end
|
14
|
+
|
15
|
+
def file_path
|
16
|
+
@file_path ||= exception.backtrace.first.match(/(.+):[0-9]+:/)[1]
|
17
|
+
end
|
18
|
+
|
19
|
+
def file_content
|
20
|
+
@file_content ||= eval? ? eval_file_content : File.read(file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def error_line_number
|
24
|
+
exception.backtrace.first.match(/:([0-9]+):/)[1].to_i
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError::Renderers
|
4
|
+
class SyntaxErrorRenderer < ExceptionRenderer
|
5
|
+
private
|
6
|
+
|
7
|
+
def file_path
|
8
|
+
@file_path ||= exception.message.match(/(.+):[0-9]+:/)[1]
|
9
|
+
end
|
10
|
+
|
11
|
+
def file_content
|
12
|
+
@file_content ||= eval? ? eval_file_content : File.read(file_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_line_number
|
16
|
+
exception.message.match(/:([0-9]+):/)[1].to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def exception_i18n_name
|
20
|
+
I18n.t 'syntax_error.title'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError::Renderers
|
4
|
+
class UnnecessaryEndErrorRenderer < SyntaxErrorRenderer
|
5
|
+
private
|
6
|
+
|
7
|
+
def error_message
|
8
|
+
I18n.t('syntax_error.unnecessary_end')
|
9
|
+
end
|
10
|
+
|
11
|
+
def display_error_detail
|
12
|
+
display_error_line_string
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyFriendlyError
|
4
|
+
class Utils
|
5
|
+
class << self
|
6
|
+
def suppress_error_display
|
7
|
+
original_stdout = $stderr
|
8
|
+
$stderr = StringIO.new
|
9
|
+
|
10
|
+
yield
|
11
|
+
ensure
|
12
|
+
$stderr = original_stdout
|
13
|
+
end
|
14
|
+
|
15
|
+
def replace_string_at string, range
|
16
|
+
regexp = Regexp.new "\\A(.{#{range.first - 1}})(.{#{range.size}})(.*)\\Z"
|
17
|
+
match_data = string.match regexp
|
18
|
+
replaced_string = yield match_data[2]
|
19
|
+
match_data[1] + replaced_string + match_data[3]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/ruby_friendly_error.rb
CHANGED
@@ -5,8 +5,17 @@ require 'i18n'
|
|
5
5
|
require 'parser/current'
|
6
6
|
|
7
7
|
require 'ruby_friendly_error/ast'
|
8
|
+
require 'ruby_friendly_error/utils'
|
8
9
|
require 'ruby_friendly_error/version'
|
9
10
|
|
11
|
+
require 'ruby_friendly_error/renderers/exception_renderer'
|
12
|
+
require 'ruby_friendly_error/renderers/syntax_error_renderer'
|
13
|
+
require 'ruby_friendly_error/renderers/missing_end_error_renderer'
|
14
|
+
require 'ruby_friendly_error/renderers/unnecessary_end_error_renderer'
|
15
|
+
require 'ruby_friendly_error/renderers/standard_error_renderer'
|
16
|
+
require 'ruby_friendly_error/renderers/name_error_renderer'
|
17
|
+
require 'ruby_friendly_error/renderers/argument_error_renderer'
|
18
|
+
|
10
19
|
Bundler.require(:development)
|
11
20
|
|
12
21
|
Parser::Builders::Default.emit_lambda = true
|
@@ -15,138 +24,55 @@ Parser::Builders::Default.emit_encoding = true
|
|
15
24
|
Parser::Builders::Default.emit_index = true
|
16
25
|
|
17
26
|
module RubyFriendlyError
|
18
|
-
|
19
|
-
|
27
|
+
module Renderers; end
|
28
|
+
|
29
|
+
ROOT_PATH = Pathname.new(__FILE__).dirname.parent.to_s
|
30
|
+
WINDOW = 2
|
31
|
+
DISPLAY_START = '<' * 80
|
32
|
+
DISPLAY_END = '>' * 80
|
20
33
|
|
21
34
|
I18n.load_path = Dir[File.join(ROOT_PATH, 'locales', '*.yml')]
|
22
35
|
I18n.backend.load_translations
|
23
|
-
I18n.backend.store_translations(:en , YAML.load_file(File.join(ROOT_PATH, 'locales', 'en.yml')))
|
24
36
|
|
25
37
|
class << self
|
26
|
-
def load file_path
|
27
|
-
exec File.read(file_path), file_path
|
38
|
+
def load file_path, lang = :en
|
39
|
+
exec File.read(file_path), File.expand_path(file_path), lang
|
28
40
|
end
|
29
41
|
|
30
|
-
def exec file_content, file_name = '(eval)'
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
42
|
+
def exec file_content, file_name = '(eval)', lang = :en
|
43
|
+
load_i18n lang
|
44
|
+
|
45
|
+
eval file_content, nil, file_name, 1 # rubocop:disable Security/Eval
|
46
|
+
rescue Exception => exception # rubocop:disable Lint/RescueException
|
47
|
+
renderer_class = renderer_class(exception)
|
48
|
+
|
49
|
+
raise exception unless renderer_class
|
50
|
+
|
51
|
+
renderer_class.new(exception, file_content).render
|
52
|
+
exit false
|
36
53
|
end
|
37
54
|
|
38
55
|
private
|
39
56
|
|
40
|
-
def
|
41
|
-
|
57
|
+
def load_i18n lang
|
58
|
+
I18n.backend.store_translations(lang, YAML.load_file(File.join(ROOT_PATH, 'locales', "#{lang}.yml")))
|
59
|
+
I18n.locale = lang
|
60
|
+
end
|
42
61
|
|
62
|
+
def renderer_class exception
|
43
63
|
case exception
|
44
64
|
when SyntaxError
|
45
65
|
case exception.message
|
46
66
|
when /unnecessary `end`/
|
47
|
-
|
67
|
+
RubyFriendlyError::Renderers::UnnecessaryEndErrorRenderer
|
48
68
|
when /unexpected end-of-input/
|
49
|
-
|
69
|
+
RubyFriendlyError::Renderers::MissingEndErrorRenderer
|
50
70
|
end
|
51
71
|
when NameError
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
raise exception
|
57
|
-
end
|
58
|
-
|
59
|
-
def render_missing_end_error file_content, ex
|
60
|
-
line = ex.message.match(/:([0-9]+):/)[1].to_i
|
61
|
-
display_error_line file_content, line
|
62
|
-
STDERR.puts I18n.t('syntax_error.title').colorize(:light_red) + ':'
|
63
|
-
STDERR.puts format_lines(I18n.t('syntax_error.missing_end')) { |l, _i| " #{l}" }.colorize(:light_red)
|
64
|
-
end
|
65
|
-
|
66
|
-
def render_unnecessary_end_error file_content, ex
|
67
|
-
line = ex.message.match(/:([0-9]+):/)[1].to_i
|
68
|
-
display_error_line file_content, line
|
69
|
-
STDERR.puts I18n.t('syntax_error.title').colorize(:light_red) + ':'
|
70
|
-
STDERR.puts format_lines(I18n.t('syntax_error.unnecessary_end')) { |l, _i| " #{l}" }.colorize(:light_red)
|
71
|
-
end
|
72
|
-
|
73
|
-
def render_name_error_with_did_you_mean file_content, ex, ast
|
74
|
-
corrections = ex.spell_checker.corrections
|
75
|
-
corrections.each do |var_name|
|
76
|
-
node = ast.find_by_variable_name var_name
|
77
|
-
display_error_line file_content, node.loc.line
|
78
|
-
end
|
79
|
-
|
80
|
-
STDERR.puts I18n.t('name_error.title').colorize(:light_red) + ':'
|
81
|
-
var_name_str = ex.spell_checker.name.underline
|
82
|
-
corrections_str = corrections.map { |c| "`#{c.to_s.underline}`" }.join(', ')
|
83
|
-
message = I18n.t('name_error.with_did_you_mean', var_name: var_name_str, corrections: corrections_str)
|
84
|
-
STDERR.puts format_lines(message) { |l, _i| " #{l}" }.colorize(:light_red)
|
85
|
-
end
|
86
|
-
|
87
|
-
def suppress_error_display
|
88
|
-
original_stdout = $stderr
|
89
|
-
$stderr = StringIO.new
|
90
|
-
|
91
|
-
yield
|
92
|
-
ensure
|
93
|
-
$stderr = original_stdout
|
94
|
-
end
|
95
|
-
|
96
|
-
def display_error_line file_content, error_line, window = WINDOW
|
97
|
-
lines = file_content.split("\n", -1)
|
98
|
-
line_size = lines.size
|
99
|
-
window_start = [error_line - window, 1].max
|
100
|
-
window_end = [error_line + window, line_size].min
|
101
|
-
|
102
|
-
if error_line > 1
|
103
|
-
start_line = window_start
|
104
|
-
end_line = error_line - 1
|
105
|
-
STDERR.puts format_lines(lines, start_line, end_line) { |l, i| "#{start_line + i}: #{l}" }&.colorize(:light_yellow)
|
106
|
-
end
|
107
|
-
STDERR.puts "#{error_line}: #{lines[error_line - 1]}".rstrip.colorize(:light_red)
|
108
|
-
if error_line < line_size
|
109
|
-
start_line = error_line + 1
|
110
|
-
end_line = window_end
|
111
|
-
STDERR.puts format_lines(lines, error_line + 1, end_line) { |l, i| "#{start_line + i}: #{l}" }&.colorize(:light_yellow)
|
112
|
-
end
|
113
|
-
STDERR.puts
|
114
|
-
end
|
115
|
-
|
116
|
-
def format_lines lines, start_line = 1, end_line = 0
|
117
|
-
lines =
|
118
|
-
case lines
|
119
|
-
when String then lines.split("\n", -1)
|
120
|
-
when Array then lines
|
121
|
-
end
|
122
|
-
lines[(start_line - 1)..(end_line - 1)]
|
123
|
-
&.map
|
124
|
-
&.with_index { |l, i| block_given? ? yield(l, i).rstrip : l.rstrip }
|
125
|
-
&.join("\n")
|
126
|
-
end
|
127
|
-
|
128
|
-
def display_backtrace file_name, backtrace
|
129
|
-
fake_backtrace = backtrace
|
130
|
-
.dup
|
131
|
-
.delete_if { |b| b.match(/ruby_friendly_error/) }
|
132
|
-
.delete_if { |b| b.match(/`block in exec'/) }
|
133
|
-
.map do |b|
|
134
|
-
if b.match? Regexp.new("#{file_name}.*`exec'")
|
135
|
-
b.sub("`exec'", "`<main>'")
|
136
|
-
else
|
137
|
-
b
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
fake_backtrace_size = fake_backtrace.size
|
142
|
-
fake_backtrace_digits = fake_backtrace_size.to_s.length
|
143
|
-
|
144
|
-
fake_backtrace.reverse_each.with_index do |b, i|
|
145
|
-
num = fake_backtrace_size - i
|
146
|
-
num_digits = num.to_s.length
|
147
|
-
STDERR.puts "#{' ' * (fake_backtrace_digits - num_digits)}#{num}:#{b}".colorize(:light_red)
|
72
|
+
RubyFriendlyError::Renderers::NameEndErrorRenderer
|
73
|
+
when ArgumentError
|
74
|
+
RubyFriendlyError::Renderers::ArgumentErrorRenderer
|
148
75
|
end
|
149
|
-
STDERR.puts
|
150
76
|
end
|
151
77
|
end
|
152
78
|
end
|
data/locales/en.yml
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
en:
|
2
|
+
main_message: " occurred"
|
2
3
|
syntax_error:
|
3
4
|
title: "syntax error"
|
4
5
|
missing_end: |
|
@@ -7,8 +8,15 @@ en:
|
|
7
8
|
unnecessary_end: |
|
8
9
|
exist unnecessary `end`.
|
9
10
|
Please remove unnecessary `end`.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
standard_error:
|
12
|
+
name_error:
|
13
|
+
title: "name error"
|
14
|
+
with_did_you_mean: |
|
15
|
+
undefined local variable or method `%{var_name}`.
|
16
|
+
Did you mean? %{corrections}.
|
17
|
+
argument_error:
|
18
|
+
title: "argument error"
|
19
|
+
wrong_number_of_arguments: |
|
20
|
+
wrong number of arguments.
|
21
|
+
`%{def_name}` expected %{arg_range}.
|
22
|
+
but given %{send_arg}.
|
data/locales/ja.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
ja:
|
2
|
+
main_message: "が発生しました。"
|
3
|
+
syntax_error:
|
4
|
+
title: "構文エラー"
|
5
|
+
missing_end: |
|
6
|
+
`end` が欠けています。
|
7
|
+
おそらくこの行より前に問題があります。
|
8
|
+
unnecessary_end: |
|
9
|
+
`end` が余分に存在しています。
|
10
|
+
不要な `end` を取り除いてください。
|
11
|
+
standard_error:
|
12
|
+
name_error:
|
13
|
+
title: "名前エラー"
|
14
|
+
with_did_you_mean: |
|
15
|
+
`%{var_name}` という名前の変数、もしくはメソッドが定義されていません。
|
16
|
+
もしかして? %{corrections}。
|
17
|
+
argument_error:
|
18
|
+
title: "引数エラー"
|
19
|
+
wrong_number_of_arguments: |
|
20
|
+
引数の数が誤っています。
|
21
|
+
`%{def_name}` は %{arg_range} 個の引数を期待しています。
|
22
|
+
しかし %{send_arg} 個の引数を渡しています。
|
data/ruby_friendly_error.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.email = ['isuke770@gmail.com']
|
12
12
|
|
13
13
|
spec.summary = 'make to ruby error messages friendly.'
|
14
|
-
spec.description = 'make to ruby error messages friendly.'
|
14
|
+
spec.description = 'make to ruby error messages friendly. Display multilingual message and error lines'
|
15
15
|
spec.homepage = 'https://github.com/isuke/ruby_friendly_error'
|
16
16
|
spec.license = 'MIT'
|
17
17
|
|
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
|
27
27
|
spec.add_dependency 'colorize', '~> 0.8'
|
28
28
|
spec.add_dependency 'i18n' , '~> 1.1'
|
29
|
-
spec.add_dependency 'parser' , '~> 2.5
|
30
|
-
spec.add_development_dependency 'bundler'
|
31
|
-
spec.add_development_dependency 'rake'
|
29
|
+
spec.add_dependency 'parser' , '~> 2.5'
|
30
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
31
|
+
spec.add_development_dependency 'rake' , '~> 10.0'
|
32
32
|
end
|
data/samples/miss_spell1.rb
CHANGED
@@ -1,20 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# 1: prayer_life = 100
|
4
|
-
# 2: player_lifee = 200
|
5
|
-
# 3:
|
6
|
-
# 4: puts 'hoge' if player_life > 0
|
7
|
-
|
8
|
-
# 1: prayer_life = 100
|
9
|
-
# 2: player_lifee = 200
|
10
|
-
# 3:
|
11
|
-
|
12
|
-
# name error:
|
13
|
-
# undefined local variable or method `player_life`
|
14
|
-
# Did you mean? `player_lifee`, `prayer_life`
|
15
|
-
|
16
3
|
prayer_life = 100
|
17
4
|
|
18
5
|
player_lifee = 100
|
19
6
|
|
20
|
-
|
7
|
+
3.times do
|
8
|
+
puts 'hoge' if player_life > 0
|
9
|
+
end
|
data/samples/not_error.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_friendly_error
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- isuke
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-09-
|
11
|
+
date: 2018-09-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: colorize
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 2.5
|
47
|
+
version: '2.5'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 2.5
|
54
|
+
version: '2.5'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: bundler
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,7 +80,8 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '10.0'
|
83
|
-
description: make to ruby error messages friendly.
|
83
|
+
description: make to ruby error messages friendly. Display multilingual message and
|
84
|
+
error lines
|
84
85
|
email:
|
85
86
|
- isuke770@gmail.com
|
86
87
|
executables:
|
@@ -108,9 +109,19 @@ files:
|
|
108
109
|
- exe/ruby_friendly_error
|
109
110
|
- lib/ruby_friendly_error.rb
|
110
111
|
- lib/ruby_friendly_error/ast.rb
|
112
|
+
- lib/ruby_friendly_error/renderers/argument_error_renderer.rb
|
113
|
+
- lib/ruby_friendly_error/renderers/exception_renderer.rb
|
114
|
+
- lib/ruby_friendly_error/renderers/missing_end_error_renderer.rb
|
115
|
+
- lib/ruby_friendly_error/renderers/name_error_renderer.rb
|
116
|
+
- lib/ruby_friendly_error/renderers/standard_error_renderer.rb
|
117
|
+
- lib/ruby_friendly_error/renderers/syntax_error_renderer.rb
|
118
|
+
- lib/ruby_friendly_error/renderers/unnecessary_end_error_renderer.rb
|
119
|
+
- lib/ruby_friendly_error/utils.rb
|
111
120
|
- lib/ruby_friendly_error/version.rb
|
112
121
|
- locales/en.yml
|
122
|
+
- locales/ja.yml
|
113
123
|
- ruby_friendly_error.gemspec
|
124
|
+
- samples/arg_error.rb
|
114
125
|
- samples/mismatch_args.rb
|
115
126
|
- samples/miss_spell1.rb
|
116
127
|
- samples/miss_spell2.rb
|