mentor 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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/Guardfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +7 -0
- data/bin/mentor +4 -0
- data/lib/errors/mentor_error.rb +52 -0
- data/lib/errors/mentor_no_method_error.rb +24 -0
- data/lib/errors/no_method_did_you_mean_suggestion_error.rb +32 -0
- data/lib/helpers/colorize.rb +156 -0
- data/lib/helpers/globals.rb +27 -0
- data/lib/helpers/output_helper.rb +37 -0
- data/lib/helpers/outputs.rb +140 -0
- data/lib/helpers/requires.rb +21 -0
- data/lib/main.rb +23 -0
- data/lib/mentor/version.rb +3 -0
- data/lib/mentor.rb +37 -0
- data/lib/sections/backtrace.rb +15 -0
- data/lib/sections/did_you_mean_correction.rb +20 -0
- data/lib/sections/error_class_specific_help.rb +16 -0
- data/lib/sections/header.rb +47 -0
- data/lib/sections/line_of_code.rb +30 -0
- data/lib/sections/lines_of_codes.rb +70 -0
- data/lib/sections/relative_path.rb +15 -0
- data/lib/sections/ruby_error_complete.rb +21 -0
- data/lib/sections/ruby_error_main.rb +77 -0
- data/lib/sections/suggestion.rb +23 -0
- data/mentor.gemspec +31 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ae3511e3b6d5760ea27f80381bec9c917f3c49fe
|
4
|
+
data.tar.gz: 0ce7c1629a9530f2e4b2f8435f331c20cc521dbb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ecc7a4768849f6cef68ea37a90b805ece2db7e01bfd401e4968077169c6bd89c70eabc61958494e5b6e0ce2603bd7fa530154eaad5587c204e0ce02b1662ad6c
|
7
|
+
data.tar.gz: df14fccab379150b55c91f6298eac72e9d9adcfb10098fdd71996f8ca6f0efb26e0bfeba346b2258008c99105baae0bda994748fbc7e444814b32f7f823094c4
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/Gemfile.lock
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Sean Lerner
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# Mentor
|
2
|
+
|
3
|
+
Mentor is a command line application for new Ruby developers. It gives descriptive error messages and where possible provides suggestions on how to best fix errors.
|
4
|
+
|
5
|
+
Instead of using Ruby to run your ruby programs like so:
|
6
|
+
|
7
|
+
```
|
8
|
+
ruby my_ruby_program.rb
|
9
|
+
```
|
10
|
+
|
11
|
+
Use Mentor:
|
12
|
+
|
13
|
+
```
|
14
|
+
mentor my_ruby_program.rb
|
15
|
+
```
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
From your command line, type:
|
20
|
+
|
21
|
+
```
|
22
|
+
gem install mentor
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
From your command line, type:
|
28
|
+
|
29
|
+
```
|
30
|
+
mentor my_ruby_program.rb
|
31
|
+
```
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitLab at https://gitlab.com/seanlerner/mentor.
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
40
|
+
|
41
|
+
## Credits
|
42
|
+
|
43
|
+
Mentor was created by Sean Lerner.
|
data/Rakefile
ADDED
data/bin/mentor
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class MentorError < StandardError
|
4
|
+
|
5
|
+
include Outputs, Colorize
|
6
|
+
|
7
|
+
def self.find
|
8
|
+
error_classes.find(&:can_handle?)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.can_handle?
|
12
|
+
Mentor.tp.raised_exception.class == RuntimeError
|
13
|
+
end
|
14
|
+
|
15
|
+
def output
|
16
|
+
puts
|
17
|
+
sections_formatted.each do |section|
|
18
|
+
puts section
|
19
|
+
puts
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.error_classes
|
26
|
+
[
|
27
|
+
NoMethodDidYouMeanSuggestionError,
|
28
|
+
MentorNoMethodError,
|
29
|
+
MentorError
|
30
|
+
]
|
31
|
+
end
|
32
|
+
|
33
|
+
def sections
|
34
|
+
[
|
35
|
+
Header.new,
|
36
|
+
RubyErrorComplete.new,
|
37
|
+
RelativePath.new,
|
38
|
+
LinesOfCode.new
|
39
|
+
]
|
40
|
+
end
|
41
|
+
|
42
|
+
def sections_formatted
|
43
|
+
sections.map do |section|
|
44
|
+
section.lines.map do |line|
|
45
|
+
colorize line
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class MentorNoMethodError < MentorError
|
4
|
+
|
5
|
+
extend Outputs, Colorize
|
6
|
+
|
7
|
+
def self.can_handle?
|
8
|
+
Mentor.tp.raised_exception.class == NoMethodError
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def sections
|
14
|
+
[
|
15
|
+
Header.new,
|
16
|
+
RubyErrorComplete.new,
|
17
|
+
RelativePath.new,
|
18
|
+
LinesOfCode.new
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class NoMethodDidYouMeanSuggestionError < MentorNoMethodError
|
4
|
+
|
5
|
+
def self.can_handle?
|
6
|
+
super && Mentor.tp.raised_exception&.corrections.any?
|
7
|
+
end
|
8
|
+
|
9
|
+
def sections
|
10
|
+
[
|
11
|
+
Header.new,
|
12
|
+
RubyErrorComplete.new,
|
13
|
+
RelativePath.new,
|
14
|
+
LinesOfCode.new,
|
15
|
+
ErrorClassSpecificHelp.new(error_class_specific_help),
|
16
|
+
Suggestion.new("Try changing the method #{method_name} to #{did_you_mean_word} on #{var_for_method}.")
|
17
|
+
]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def error_class_specific_help
|
23
|
+
[
|
24
|
+
"#{var_for_method} does not have the method #{method_name}.",
|
25
|
+
'',
|
26
|
+
'You may have made a typo.'
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
module Colorize
|
4
|
+
|
5
|
+
def colorize(line)
|
6
|
+
|
7
|
+
if line_of_code?(line)
|
8
|
+
line = syntax_highlight(line)
|
9
|
+
elsif backtrace?(line)
|
10
|
+
return rainbow(line, :backtrace_line)
|
11
|
+
end
|
12
|
+
|
13
|
+
apply_colors(line)
|
14
|
+
end
|
15
|
+
|
16
|
+
def colorize_section
|
17
|
+
@lines.map! do |line|
|
18
|
+
line = rainbow(line, :suggestion)
|
19
|
+
colorize(line)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def backtrace?(line)
|
26
|
+
line['from ']
|
27
|
+
end
|
28
|
+
|
29
|
+
def line_of_code?(line)
|
30
|
+
line[padded_lineno_with_colon_regex]
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply_colors(line)
|
34
|
+
|
35
|
+
line = color_pattern(line, error_lineno, "#{file_name}:#{error_lineno}", :error_lineno)
|
36
|
+
line = color_pattern(line, ruby_error_class, "(#{ruby_error_class})", :ruby_error_class)
|
37
|
+
line = color_pattern(line, ruby_error_class, " #{ruby_error_class} ", :ruby_error_class)
|
38
|
+
|
39
|
+
output_types = %i(
|
40
|
+
did_you_mean_text did_you_mean_word
|
41
|
+
message horizontal_line ruby_error_text
|
42
|
+
calling_method method_class method_name var_for_method
|
43
|
+
absolute_base_dir relative_base_dir app_dir file_name
|
44
|
+
)
|
45
|
+
|
46
|
+
output_types.reduce(line) do |line, output_type|
|
47
|
+
color_text(line, send(output_type), output_type)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def syntax_highlight(line)
|
53
|
+
padded_lineno = line[padded_lineno_with_colon_regex]
|
54
|
+
code = line[padded_lineno.size..-1]
|
55
|
+
color_padded_lineno(padded_lineno) + color_code(code)
|
56
|
+
end
|
57
|
+
|
58
|
+
def color_code(code)
|
59
|
+
formatter = Rouge::Formatters::Terminal256.new
|
60
|
+
lexer = Rouge::Lexers::Ruby.new
|
61
|
+
formatter.format(lexer.lex(code))
|
62
|
+
end
|
63
|
+
|
64
|
+
def color_padded_lineno(padded_lineno)
|
65
|
+
text_to_color = padded_lineno[padded_lineno_regex]
|
66
|
+
color = text_to_color['=>'] ? :error_lineno : :subtle
|
67
|
+
color_text(padded_lineno, text_to_color, color)
|
68
|
+
end
|
69
|
+
|
70
|
+
def color_pattern(full_text, text_to_color, match_pattern, output_type)
|
71
|
+
full_text.gsub(match_pattern, color_text(match_pattern, text_to_color, output_type))
|
72
|
+
end
|
73
|
+
|
74
|
+
def color_text(full_text, text_to_color, output_type)
|
75
|
+
|
76
|
+
return full_text unless text_to_color && text_to_color.size > 0
|
77
|
+
|
78
|
+
colorized_text = rainbow(text_to_color, output_type)
|
79
|
+
remaining_text = full_text
|
80
|
+
text_processed = ''
|
81
|
+
|
82
|
+
while remaining_text[text_to_color]
|
83
|
+
before_text = before_text_to_color(remaining_text, text_to_color)
|
84
|
+
reset_color = get_reset_color(before_text).to_s
|
85
|
+
text_processed += before_text + colorized_text
|
86
|
+
index_after_text_processed = (before_text + text_to_color).size
|
87
|
+
remaining_text = reset_color + remaining_text[index_after_text_processed..-1].to_s
|
88
|
+
end
|
89
|
+
|
90
|
+
text_processed + remaining_text
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
def before_text_to_color(remaining_text, text_to_color)
|
95
|
+
index = remaining_text.index(text_to_color)
|
96
|
+
return '' if !index || index == 0
|
97
|
+
remaining_text[0..index - 1]
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_reset_color(before_text)
|
101
|
+
index_of_color_to_reset_to = before_text.rindex(color_regex)
|
102
|
+
before_text[index_of_color_to_reset_to..-1][color_regex] if index_of_color_to_reset_to
|
103
|
+
end
|
104
|
+
|
105
|
+
def color_regex
|
106
|
+
/\e\[(\d|;)+m/
|
107
|
+
end
|
108
|
+
|
109
|
+
def padded_lineno_regex
|
110
|
+
/^\s+(=>)?\s\d+/
|
111
|
+
end
|
112
|
+
|
113
|
+
def padded_lineno_with_colon_regex
|
114
|
+
/^\s+(=>)?\s\d+:/
|
115
|
+
end
|
116
|
+
|
117
|
+
def rainbow(str, output_type)
|
118
|
+
Rainbow(str).color(colors[output_type])
|
119
|
+
end
|
120
|
+
|
121
|
+
def colors
|
122
|
+
error = :tomato
|
123
|
+
error_lineno = :gold
|
124
|
+
method_name = :deepskyblue
|
125
|
+
subtle = :olive
|
126
|
+
very_subtle = :dimgray
|
127
|
+
|
128
|
+
{
|
129
|
+
app_dir: subtle,
|
130
|
+
backtrace_line: very_subtle,
|
131
|
+
absolute_base_dir: subtle,
|
132
|
+
relative_base_dir: subtle,
|
133
|
+
calling_method: :green,
|
134
|
+
did_you_mean_text: :royalblue,
|
135
|
+
did_you_mean_word: method_name,
|
136
|
+
error_lineno: error_lineno,
|
137
|
+
error_lineno_padded: error_lineno,
|
138
|
+
file_name: :greenyellow,
|
139
|
+
horizontal_line: :red,
|
140
|
+
lineno_subtle_padded: subtle,
|
141
|
+
message: error,
|
142
|
+
method_class: :mediumpurple,
|
143
|
+
method_name: method_name,
|
144
|
+
other_class_or_module: :darksalmon,
|
145
|
+
prominent: :ivory,
|
146
|
+
ruby_error_class: :orangered,
|
147
|
+
ruby_error_text: error,
|
148
|
+
subtle: subtle,
|
149
|
+
suggestion: :lightgreen,
|
150
|
+
var_for_method: :palevioletred
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
module Globals
|
4
|
+
|
5
|
+
DIR = __dir__ + '/../..'
|
6
|
+
|
7
|
+
attr_accessor :cancel_exit_after_error, :tp
|
8
|
+
|
9
|
+
def cancel_exit_after_error?
|
10
|
+
@cancel_exit_after_error
|
11
|
+
end
|
12
|
+
|
13
|
+
def enable
|
14
|
+
@enabled = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def disable
|
18
|
+
@enabled = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def enabled?
|
22
|
+
@enabled == true
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
module OutputHelper
|
4
|
+
|
5
|
+
def home_to_tilde(path)
|
6
|
+
path.sub(ENV['HOME'], '~')
|
7
|
+
end
|
8
|
+
|
9
|
+
def indent_lines(*lines, indent: 2)
|
10
|
+
lines.flatten!
|
11
|
+
|
12
|
+
indent.downto(1).each do |number_of_spaces|
|
13
|
+
if lines.all? { |line| line.size + indent * 2 <= terminal_width }
|
14
|
+
return lines.map { |line| ' ' * number_of_spaces + line }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
lines
|
19
|
+
end
|
20
|
+
|
21
|
+
def lines_from_file
|
22
|
+
return @lines_from_file if @lines_from_file
|
23
|
+
file = File.new(Mentor.tp.path)
|
24
|
+
@lines_from_file = file.map { |line| [file.lineno, line.chomp] }.to_h
|
25
|
+
end
|
26
|
+
|
27
|
+
def terminal_width
|
28
|
+
`tput cols`.to_i
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid_var_name
|
32
|
+
/(@@|@|\$)?[a-zA-Z][a-zA-Z_0-9]*/
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
module Outputs
|
4
|
+
|
5
|
+
include OutputHelper
|
6
|
+
|
7
|
+
def absolute_base_dir
|
8
|
+
Dir.pwd + '/'
|
9
|
+
end
|
10
|
+
|
11
|
+
def app_dir
|
12
|
+
Mentor.tp.path. # Full path
|
13
|
+
sub(absolute_base_dir, ''). # Remove path up until this file
|
14
|
+
split('/')[0..-2]. # Split path into parts and exclude filename
|
15
|
+
join('/') + # Join with '/'
|
16
|
+
'/' # Add one more '/' to end of it
|
17
|
+
end
|
18
|
+
|
19
|
+
def backtrace_lines
|
20
|
+
|
21
|
+
# Mentor.tp.raised_exception.backtrace disappears after accessing it once
|
22
|
+
# So we just do it once and then save the result to @@backtrace_lines
|
23
|
+
if self.class.class_variable_defined?(:@@backtrace_lines)
|
24
|
+
return @@backtrace_lines
|
25
|
+
end
|
26
|
+
|
27
|
+
bt_lines = Mentor.tp.raised_exception.
|
28
|
+
backtrace[1..-1]. # Remove first
|
29
|
+
grep_v(/bin\/mentor/). # Remove mentor involvement
|
30
|
+
map { |line| line.sub!(Dir.pwd + '/', '') }. # Remove path before current dir
|
31
|
+
map { |line| 'from ' + line } # Add 'from'
|
32
|
+
|
33
|
+
return bt_lines if bt_lines.empty?
|
34
|
+
|
35
|
+
# Make it appear so backtrace came from `<main>' instead of being required by mentor
|
36
|
+
bt_lines[-1].gsub!("`<top (required)>'", "`<main>'")
|
37
|
+
|
38
|
+
@@backtrace_lines = bt_lines
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def calling_method
|
43
|
+
(Mentor.tp.method_id || '<main>').to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def did_you_mean_word
|
47
|
+
if Mentor.tp.raised_exception.respond_to? :corrections
|
48
|
+
Mentor.tp.raised_exception.corrections.first&.to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def did_you_mean_text
|
53
|
+
'Did you mean?'
|
54
|
+
end
|
55
|
+
|
56
|
+
def error_lineno_padded(width)
|
57
|
+
"=> #{@lineno}".rjust(width)
|
58
|
+
end
|
59
|
+
|
60
|
+
def error_lineno
|
61
|
+
Mentor.tp.lineno.to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def file_name
|
65
|
+
Mentor.tp.path.split('/').last
|
66
|
+
end
|
67
|
+
|
68
|
+
def horizontal_line
|
69
|
+
'─' * terminal_width
|
70
|
+
end
|
71
|
+
|
72
|
+
def instance_methods
|
73
|
+
Mentor.tp.raised_exception.receiver.class.instance_methods -
|
74
|
+
Mentor.tp.raised_exception.receiver.class.superclass.instance_methods
|
75
|
+
end
|
76
|
+
|
77
|
+
def lineno_subtle_padded(width)
|
78
|
+
@lineno.to_s.rjust(width)
|
79
|
+
end
|
80
|
+
|
81
|
+
def message
|
82
|
+
if Mentor.tp.raised_exception.respond_to? :original_message
|
83
|
+
Mentor.tp.raised_exception.original_message
|
84
|
+
else
|
85
|
+
Mentor.tp.raised_exception.message
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def method_name
|
90
|
+
if Mentor.tp.raised_exception.respond_to? :spell_checker
|
91
|
+
if Mentor.tp.raised_exception.spell_checker.respond_to?(:method_name)
|
92
|
+
Mentor.tp.raised_exception.spell_checker.method_name.to_s
|
93
|
+
else
|
94
|
+
raise "cannot determine method name"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def method_class
|
100
|
+
if Mentor.tp.raised_exception.respond_to? :receiver
|
101
|
+
Mentor.tp.raised_exception.receiver.class.to_s
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def method_class_plural
|
106
|
+
pluralize method_class
|
107
|
+
end
|
108
|
+
|
109
|
+
def method_class_superclass
|
110
|
+
Mentor.tp.raised_exception.receiver.class.superclass
|
111
|
+
end
|
112
|
+
|
113
|
+
def method_class_superclasses
|
114
|
+
to_sentence(Mentor.tp.raised_exception.receiver.class.ancestors - typical_classes)
|
115
|
+
end
|
116
|
+
|
117
|
+
def relative_base_dir
|
118
|
+
home_to_tilde(absolute_base_dir)
|
119
|
+
end
|
120
|
+
|
121
|
+
def ruby_error_class
|
122
|
+
Mentor.tp.raised_exception.class.to_s
|
123
|
+
end
|
124
|
+
|
125
|
+
def ruby_error_text
|
126
|
+
'Ruby Error:'
|
127
|
+
end
|
128
|
+
|
129
|
+
def var_for_method
|
130
|
+
culprit_line = lines_from_file[Mentor.tp.lineno]
|
131
|
+
var = culprit_line[/#{valid_var_name}\.#{method_name}/].to_s.chomp(".#{method_name}")
|
132
|
+
if var.empty?
|
133
|
+
culprit_line[/#{valid_var_name} method_name/].to_s.chomp(".#{method_name}")
|
134
|
+
end
|
135
|
+
var
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Mentor
|
2
|
+
gems = [
|
3
|
+
'pry',
|
4
|
+
'rainbow',
|
5
|
+
'rouge',
|
6
|
+
]
|
7
|
+
|
8
|
+
helpers = [
|
9
|
+
'output_helper',
|
10
|
+
'outputs',
|
11
|
+
'colorize'
|
12
|
+
].map { |file| Globals::DIR + '/lib/helpers/' + file }
|
13
|
+
|
14
|
+
sections = Dir.glob(Globals::DIR + '/lib/sections/*.rb')
|
15
|
+
errors = Dir.glob(Globals::DIR + '/lib/errors/*.rb')
|
16
|
+
|
17
|
+
(gems + helpers + sections + errors).each { |file| require file }
|
18
|
+
|
19
|
+
require_relative '../main'
|
20
|
+
|
21
|
+
end
|
data/lib/main.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class Main
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
|
7
|
+
error_class = MentorError.find
|
8
|
+
|
9
|
+
return unless error_class
|
10
|
+
|
11
|
+
begin
|
12
|
+
error = error_class.new
|
13
|
+
raise error
|
14
|
+
rescue *MentorError.error_classes
|
15
|
+
error.output
|
16
|
+
exit 1 unless Mentor.cancel_exit_after_error?
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/mentor.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'mentor/version'
|
2
|
+
|
3
|
+
module Mentor
|
4
|
+
|
5
|
+
class Init
|
6
|
+
|
7
|
+
def self.setup
|
8
|
+
require_relative 'helpers/globals'
|
9
|
+
Mentor.extend(Globals)
|
10
|
+
setup_trace_point
|
11
|
+
Mentor.enable
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def self.setup_trace_point
|
17
|
+
|
18
|
+
TracePoint.trace(:raise) do |tp|
|
19
|
+
tp.disable
|
20
|
+
|
21
|
+
if Mentor.enabled?
|
22
|
+
Mentor.disable
|
23
|
+
require_relative 'helpers/requires'
|
24
|
+
Mentor.tp = tp
|
25
|
+
Main.new
|
26
|
+
end
|
27
|
+
|
28
|
+
tp.enable
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
Mentor::Init.setup
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class DidYouMeanCorrection
|
4
|
+
|
5
|
+
include Outputs
|
6
|
+
|
7
|
+
attr_reader :line
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@line = "#{did_you_mean_text} #{did_you_mean_word}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.can_handle?
|
14
|
+
Mentor.tp.raised_exception.respond_to?(:corrections) &&
|
15
|
+
Mentor.tp.raised_exception.corrections.any?
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class Header
|
4
|
+
|
5
|
+
include Outputs, Colorize
|
6
|
+
|
7
|
+
attr_reader :lines
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@lines = [
|
11
|
+
horizontal_line,
|
12
|
+
title_indented,
|
13
|
+
horizontal_line,
|
14
|
+
]
|
15
|
+
|
16
|
+
colorize_section
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def title_indented
|
22
|
+
if title_bar_spacer_size > 4
|
23
|
+
" #{ruby_error_text} #{ruby_error_class}#{title_bar_spacer_with_indent}#{file_name}:#{error_lineno}"
|
24
|
+
elsif title_bar_spacer_size > 2
|
25
|
+
"#{ruby_error_text} #{ruby_error_class}#{title_bar_spacer_without_indent}#{file_name}:#{error_lineno}"
|
26
|
+
else
|
27
|
+
"#{ruby_error_text} #{ruby_error_class}\n#{file_name}:#{error_lineno}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def title_bar_spacer_size
|
32
|
+
start_of_line = ruby_error_text + ' ' + ruby_error_class
|
33
|
+
end_of_line = file_name + ':' + error_lineno
|
34
|
+
terminal_width - (start_of_line.length + end_of_line.length)
|
35
|
+
end
|
36
|
+
|
37
|
+
def title_bar_spacer_without_indent
|
38
|
+
' ' * title_bar_spacer_size
|
39
|
+
end
|
40
|
+
|
41
|
+
def title_bar_spacer_with_indent
|
42
|
+
' ' * (title_bar_spacer_size - 2)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class LineOfCode < String
|
4
|
+
|
5
|
+
include Outputs
|
6
|
+
|
7
|
+
def initialize(lineno, line, max_length)
|
8
|
+
@lineno = lineno
|
9
|
+
@line = line
|
10
|
+
@max_length = max_length
|
11
|
+
super("#{padded_lineno}: #{@line}")
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def padded_lineno
|
17
|
+
if error_lineno?
|
18
|
+
error_lineno_padded(@max_length)
|
19
|
+
else
|
20
|
+
lineno_subtle_padded(@max_length)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def error_lineno?
|
25
|
+
@lineno == Mentor.tp.lineno
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class LinesOfCode
|
4
|
+
|
5
|
+
include Outputs
|
6
|
+
|
7
|
+
attr_accessor :lines
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@lines = indent_lines(lines_of_code_from_method_or_file, indent: 4)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def lines_of_code_from_method_or_file
|
16
|
+
if calling_method_can_be_obtained?
|
17
|
+
set_first_and_last_from_method
|
18
|
+
else
|
19
|
+
set_first_and_last_from_file
|
20
|
+
end
|
21
|
+
|
22
|
+
lineno_max_length = "=> #{@last_lineno}".length
|
23
|
+
|
24
|
+
(@first_lineno..@last_lineno).map do |lineno|
|
25
|
+
LineOfCode.new(lineno, lines_from_file[lineno], lineno_max_length)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def calling_method_can_be_obtained?
|
30
|
+
calling_method != '<main>'
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_first_and_last_from_method
|
34
|
+
method_text = Pry::Method.from_str("#{Mentor.tp.defined_class}##{calling_method}").source.split("\n")
|
35
|
+
@first_lineno = lines_from_file.select { |lineno, line| line["def #{calling_method}"] }.sort.last.first
|
36
|
+
@last_lineno = @first_lineno + method_text.size - 1
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_first_and_last_from_file
|
40
|
+
@first_lineno = Mentor.tp.lineno
|
41
|
+
@first_lineno -= 1 until first_line_at_start_of_file? || first_line_empty? ||
|
42
|
+
line_prior_to_first_line_ends?
|
43
|
+
@last_lineno = Mentor.tp.lineno
|
44
|
+
@last_lineno += 1 until last_line_at_end_of_file? || last_line_empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def first_line_at_start_of_file?
|
48
|
+
@first_lineno == 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def first_line_empty?
|
52
|
+
lines_from_file[@first_lineno].empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def line_prior_to_first_line_ends?
|
56
|
+
lines_from_file[@first_lineno - 1]['end']
|
57
|
+
end
|
58
|
+
|
59
|
+
def last_line_at_end_of_file?
|
60
|
+
@last_lineno == lines_from_file.size
|
61
|
+
end
|
62
|
+
|
63
|
+
def last_line_empty?
|
64
|
+
lines_from_file[@last_lineno].empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class RubyErrorComplete
|
4
|
+
|
5
|
+
include Outputs, Colorize
|
6
|
+
|
7
|
+
attr_reader :lines
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@lines = []
|
11
|
+
@lines << RubyErrorMain.new.lines
|
12
|
+
@lines << DidYouMeanCorrection.new.line if DidYouMeanCorrection.can_handle?
|
13
|
+
@lines << Backtrace.new.lines
|
14
|
+
@lines = indent_lines(@lines)
|
15
|
+
@lines << ''
|
16
|
+
@lines << horizontal_line
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class RubyErrorMain
|
4
|
+
|
5
|
+
include Outputs
|
6
|
+
|
7
|
+
attr_reader :lines
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@lines = ruby_error_main
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def single_line
|
16
|
+
parts = []
|
17
|
+
parts << path_and_lineno
|
18
|
+
parts << message
|
19
|
+
parts << bracketed_class
|
20
|
+
parts.flatten.join(' ')
|
21
|
+
end
|
22
|
+
|
23
|
+
def path_and_lineno
|
24
|
+
app_path_and_lineno = "#{app_dir}#{file_name}:#{error_lineno}:in `#{calling_method}':"
|
25
|
+
|
26
|
+
if backtrace_lines.any?
|
27
|
+
absolute_base_dir + app_path_and_lineno
|
28
|
+
else
|
29
|
+
app_path_and_lineno
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def bracketed_class
|
34
|
+
"(#{ruby_error_class})"
|
35
|
+
end
|
36
|
+
|
37
|
+
def ruby_error_main
|
38
|
+
if single_line.size <= terminal_width
|
39
|
+
[single_line]
|
40
|
+
else
|
41
|
+
ruby_error_wrapped
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def ruby_error_wrapped
|
46
|
+
if fit_on_one_or_two_lines?(path_and_lineno, message)
|
47
|
+
[path_and_lineno + ' ' + message, bracketed_class]
|
48
|
+
|
49
|
+
elsif fit_on_one_or_two_lines?(message, bracketed_class)
|
50
|
+
[path_and_lineno, message + ' ' + bracketed_class]
|
51
|
+
|
52
|
+
else
|
53
|
+
[path_and_lineno, message, bracketed_class]
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def fit_on_one_or_two_lines?(part_a, part_b)
|
59
|
+
fit_on_one_line?(part_a, part_b) ||
|
60
|
+
does_not_fit_on_one_line?(part_a) && fit_on_two_lines?(part_a, part_b)
|
61
|
+
end
|
62
|
+
|
63
|
+
def fit_on_one_line?(part_a, part_b)
|
64
|
+
(part_a + ' ' + part_b).size <= terminal_width
|
65
|
+
end
|
66
|
+
|
67
|
+
def does_not_fit_on_one_line?(part)
|
68
|
+
part.size >= terminal_width
|
69
|
+
end
|
70
|
+
|
71
|
+
def fit_on_two_lines?(part_a, part_b)
|
72
|
+
(part_a + ' ' + part_b).size <= terminal_width * 2
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Mentor
|
2
|
+
|
3
|
+
class Suggestion
|
4
|
+
|
5
|
+
include Outputs, Colorize
|
6
|
+
|
7
|
+
attr_reader :lines
|
8
|
+
|
9
|
+
def initialize(suggestion = '')
|
10
|
+
@lines = suggestion
|
11
|
+
format_lines
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def format_lines
|
17
|
+
@lines = indent_lines(@lines)
|
18
|
+
colorize_section
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/mentor.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mentor/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'mentor'
|
8
|
+
spec.version = Mentor::VERSION
|
9
|
+
spec.authors = ['Sean Lerner']
|
10
|
+
spec.email = ['sean@smallcity.ca']
|
11
|
+
spec.summary = 'Helpful error messages for new ruby programmers.'
|
12
|
+
spec.homepage = 'https://gitlab.com/seanlerner/mentor'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
|
19
|
+
spec.executables << 'mentor'
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'pry', '~> 0.10'
|
23
|
+
spec.add_dependency 'rainbow', '~> 2.2'
|
24
|
+
spec.add_dependency 'rouge', '~> 2.0'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
27
|
+
spec.add_development_dependency 'guard', '~> 2.14'
|
28
|
+
spec.add_development_dependency 'guard-minitest', '~> 2.4'
|
29
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mentor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sean Lerner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pry
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rainbow
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rouge
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.13'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.13'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.14'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.14'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: guard-minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '5.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '5.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '10.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '10.0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- sean@smallcity.ca
|
128
|
+
executables:
|
129
|
+
- mentor
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- ".gitignore"
|
134
|
+
- Gemfile
|
135
|
+
- Guardfile
|
136
|
+
- LICENSE.txt
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- bin/mentor
|
140
|
+
- lib/errors/mentor_error.rb
|
141
|
+
- lib/errors/mentor_no_method_error.rb
|
142
|
+
- lib/errors/no_method_did_you_mean_suggestion_error.rb
|
143
|
+
- lib/helpers/colorize.rb
|
144
|
+
- lib/helpers/globals.rb
|
145
|
+
- lib/helpers/output_helper.rb
|
146
|
+
- lib/helpers/outputs.rb
|
147
|
+
- lib/helpers/requires.rb
|
148
|
+
- lib/main.rb
|
149
|
+
- lib/mentor.rb
|
150
|
+
- lib/mentor/version.rb
|
151
|
+
- lib/sections/backtrace.rb
|
152
|
+
- lib/sections/did_you_mean_correction.rb
|
153
|
+
- lib/sections/error_class_specific_help.rb
|
154
|
+
- lib/sections/header.rb
|
155
|
+
- lib/sections/line_of_code.rb
|
156
|
+
- lib/sections/lines_of_codes.rb
|
157
|
+
- lib/sections/relative_path.rb
|
158
|
+
- lib/sections/ruby_error_complete.rb
|
159
|
+
- lib/sections/ruby_error_main.rb
|
160
|
+
- lib/sections/suggestion.rb
|
161
|
+
- mentor.gemspec
|
162
|
+
homepage: https://gitlab.com/seanlerner/mentor
|
163
|
+
licenses:
|
164
|
+
- MIT
|
165
|
+
metadata: {}
|
166
|
+
post_install_message:
|
167
|
+
rdoc_options: []
|
168
|
+
require_paths:
|
169
|
+
- lib
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
requirements: []
|
181
|
+
rubyforge_project:
|
182
|
+
rubygems_version: 2.6.11
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: Helpful error messages for new ruby programmers.
|
186
|
+
test_files: []
|