ruby_friendly_error 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|