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 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
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,3 @@
1
+ guard :minitest do
2
+ watch(/lib|test|\./) { 'test/mentor_test.rb' }
3
+ end
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
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ task default: :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = 'test/*_test.rb'
7
+ end
data/bin/mentor ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'mentor'
3
+ Mentor.enable
4
+ require_relative Dir.pwd + '/' + ARGV.first
@@ -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
@@ -0,0 +1,3 @@
1
+ module Mentor
2
+ VERSION = '0.1.0'
3
+ 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,15 @@
1
+ module Mentor
2
+
3
+ class Backtrace
4
+
5
+ include Outputs
6
+
7
+ attr_reader :lines
8
+
9
+ def initialize
10
+ @lines = indent_lines(backtrace_lines, indent: 10)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -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,16 @@
1
+ module Mentor
2
+
3
+ class ErrorClassSpecificHelp
4
+
5
+ include Outputs, Colorize
6
+
7
+ attr_reader :lines
8
+
9
+ def initialize(lines)
10
+ @lines = indent_lines(lines)
11
+ colorize_section
12
+ end
13
+
14
+ end
15
+
16
+ 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,15 @@
1
+ module Mentor
2
+
3
+ class RelativePath
4
+
5
+ include Outputs
6
+
7
+ attr_accessor :lines
8
+
9
+ def initialize
10
+ @lines = indent_lines(relative_base_dir + app_dir + file_name + ':' + error_lineno)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -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: []