mentor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|