seeing_is_believing 3.0.0.beta.4 → 3.0.0.beta.5

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -8
  3. data/Rakefile +1 -1
  4. data/Readme.md +65 -25
  5. data/bin/seeing_is_believing +1 -0
  6. data/docs/sib-streaming.gif +0 -0
  7. data/features/deprecated-flags.feature +62 -2
  8. data/features/errors.feature +12 -7
  9. data/features/examples.feature +143 -4
  10. data/features/flags.feature +89 -29
  11. data/features/regression.feature +58 -14
  12. data/features/support/env.rb +4 -0
  13. data/features/xmpfilter-style.feature +181 -36
  14. data/lib/seeing_is_believing.rb +44 -33
  15. data/lib/seeing_is_believing/binary.rb +31 -88
  16. data/lib/seeing_is_believing/binary/align_chunk.rb +30 -11
  17. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +10 -16
  18. data/lib/seeing_is_believing/binary/annotate_every_line.rb +5 -25
  19. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +136 -0
  20. data/lib/seeing_is_believing/binary/comment_lines.rb +8 -10
  21. data/lib/seeing_is_believing/binary/commentable_lines.rb +20 -26
  22. data/lib/seeing_is_believing/binary/config.rb +392 -0
  23. data/lib/seeing_is_believing/binary/data_structures.rb +57 -0
  24. data/lib/seeing_is_believing/binary/engine.rb +104 -0
  25. data/lib/seeing_is_believing/binary/{comment_formatter.rb → format_comment.rb} +6 -6
  26. data/lib/seeing_is_believing/binary/remove_annotations.rb +29 -28
  27. data/lib/seeing_is_believing/binary/rewrite_comments.rb +42 -43
  28. data/lib/seeing_is_believing/code.rb +105 -49
  29. data/lib/seeing_is_believing/debugger.rb +6 -5
  30. data/lib/seeing_is_believing/error.rb +6 -17
  31. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +78 -129
  32. data/lib/seeing_is_believing/event_stream/consumer.rb +114 -64
  33. data/lib/seeing_is_believing/event_stream/events.rb +169 -11
  34. data/lib/seeing_is_believing/event_stream/handlers/debug.rb +57 -0
  35. data/lib/seeing_is_believing/event_stream/handlers/record_exitstatus.rb +18 -0
  36. data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +45 -0
  37. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +39 -0
  38. data/lib/seeing_is_believing/event_stream/producer.rb +25 -24
  39. data/lib/seeing_is_believing/hash_struct.rb +206 -0
  40. data/lib/seeing_is_believing/result.rb +20 -3
  41. data/lib/seeing_is_believing/the_matrix.rb +20 -12
  42. data/lib/seeing_is_believing/version.rb +1 -1
  43. data/lib/seeing_is_believing/wrap_expressions.rb +55 -115
  44. data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +14 -0
  45. data/seeing_is_believing.gemspec +1 -1
  46. data/spec/binary/alignment_specs.rb +27 -0
  47. data/spec/binary/comment_lines_spec.rb +3 -2
  48. data/spec/binary/config_spec.rb +657 -0
  49. data/spec/binary/engine_spec.rb +97 -0
  50. data/spec/binary/{comment_formatter_spec.rb → format_comment_spec.rb} +2 -2
  51. data/spec/binary/marker_spec.rb +71 -0
  52. data/spec/binary/options_spec.rb +0 -0
  53. data/spec/binary/remove_annotations_spec.rb +31 -18
  54. data/spec/binary/rewrite_comments_spec.rb +26 -11
  55. data/spec/code_spec.rb +190 -6
  56. data/spec/debugger_spec.rb +4 -0
  57. data/spec/evaluate_by_moving_files_spec.rb +38 -20
  58. data/spec/event_stream_spec.rb +265 -116
  59. data/spec/hash_struct_spec.rb +514 -0
  60. data/spec/seeing_is_believing_spec.rb +108 -46
  61. data/spec/spec_helper.rb +9 -0
  62. data/spec/wrap_expressions_spec.rb +207 -172
  63. metadata +30 -18
  64. data/docs/for-presentations +0 -33
  65. data/lib/seeing_is_believing/binary/annotate_xmpfilter_style.rb +0 -128
  66. data/lib/seeing_is_believing/binary/interpret_flags.rb +0 -156
  67. data/lib/seeing_is_believing/binary/parse_args.rb +0 -263
  68. data/lib/seeing_is_believing/event_stream/update_result.rb +0 -24
  69. data/lib/seeing_is_believing/inspect_expressions.rb +0 -21
  70. data/lib/seeing_is_believing/parser_helpers.rb +0 -82
  71. data/spec/binary/interpret_flags_spec.rb +0 -332
  72. data/spec/binary/parse_args_spec.rb +0 -415
@@ -0,0 +1,57 @@
1
+ require 'seeing_is_believing/hash_struct'
2
+
3
+ class SeeingIsBelieving
4
+ module Binary
5
+ class Markers < HashStruct
6
+ attribute(:value) { Marker.new prefix: '# => ', regex: '^#\s*=>\s*' }
7
+ attribute(:exception) { Marker.new prefix: '# ~> ', regex: '^#\s*~>\s*' }
8
+ attribute(:stdout) { Marker.new prefix: '# >> ', regex: '^#\s*>>\s*' }
9
+ attribute(:stderr) { Marker.new prefix: '# !> ', regex: '^#\s*!>\s*' }
10
+ end
11
+
12
+
13
+ class Marker < HashStruct
14
+ def self.to_regex(string)
15
+ return string if string.kind_of? Regexp
16
+ flag_to_bit = {
17
+ 'i' => 0b001,
18
+ 'x' => 0b010,
19
+ 'm' => 0b100
20
+ }
21
+ string =~ %r{\A/(.*)/([mxi]*)\Z}
22
+ body = $1 || string
23
+ flags = ($2 || "").each_char.inject(0) { |bits, flag| bits | flag_to_bit[flag] }
24
+ Regexp.new body, flags
25
+ end
26
+
27
+ attribute :prefix # e.g. "# => "
28
+ attribute :regex # identify prefix in a comment, e.g. /^# => /
29
+
30
+ def []=(key, value)
31
+ value = Marker.to_regex(value) if key == :regex
32
+ super key, value
33
+ end
34
+ end
35
+
36
+
37
+ class AnnotatorOptions < HashStruct
38
+ attribute(:alignment_strategy) { AlignChunk }
39
+ attribute(:markers) { Markers.new }
40
+ attribute(:max_line_length) { Float::INFINITY }
41
+ attribute(:max_result_length) { Float::INFINITY }
42
+ end
43
+
44
+
45
+ class ErrorMessage < HashStruct.for(:explanation)
46
+ def to_s
47
+ "Error: #{explanation}"
48
+ end
49
+ end
50
+
51
+ class SyntaxErrorMessage < ErrorMessage.for(:line_number, :filename)
52
+ def to_s
53
+ "Syntax Error: #{filename}:#{line_number}\n#{explanation}\n"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,104 @@
1
+ require 'seeing_is_believing/code'
2
+ require 'seeing_is_believing/binary/data_structures'
3
+ require 'seeing_is_believing/binary/remove_annotations'
4
+ require 'seeing_is_believing/event_stream/handlers/record_exitstatus'
5
+
6
+ class SeeingIsBelieving
7
+ module Binary
8
+ class MustEvaluateFirst < SeeingIsBelievingError
9
+ def initialize(method)
10
+ super "Cannot call #{method} on engine until it has evaluated the program."
11
+ end
12
+ end
13
+
14
+ class Engine
15
+ def initialize(config)
16
+ self.config = config
17
+ end
18
+
19
+ def cleaned_body
20
+ @cleaned_body ||= if missing_newline?
21
+ normalized_cleaned_body.chomp!
22
+ else
23
+ normalized_cleaned_body
24
+ end
25
+ end
26
+
27
+ def syntax_error?
28
+ code.syntax.invalid?
29
+ end
30
+
31
+ def syntax_error
32
+ return unless syntax_error?
33
+ SyntaxErrorMessage.new line_number: code.syntax.line_number,
34
+ explanation: code.syntax.error_message,
35
+ filename: config.lib_options.filename
36
+ end
37
+
38
+ def evaluate!
39
+ @evaluated || begin
40
+ SeeingIsBelieving.call normalized_cleaned_body,
41
+ config.lib_options.merge(event_handler: record_exitstatus)
42
+ @timed_out = false
43
+ @evaluated = true
44
+ end
45
+ rescue Timeout::Error
46
+ @timed_out = true
47
+ @evaluated = true
48
+ ensure return self unless $! # idk, maybe too tricky, but was really annoying putting it in three places
49
+ end
50
+
51
+ def timed_out?
52
+ @evaluated || raise(MustEvaluateFirst.new __method__)
53
+ @timed_out
54
+ end
55
+
56
+ def result
57
+ @evaluated || raise(MustEvaluateFirst.new __method__)
58
+ config.lib_options.event_handler.result # The stream handler will not have a result (implies this was used wrong)
59
+ end
60
+
61
+ def annotated_body
62
+ @annotated_body ||= begin
63
+ @evaluated || raise(MustEvaluateFirst.new __method__)
64
+ annotated = config.annotator.call normalized_cleaned_body,
65
+ result,
66
+ config.annotator_options.to_h
67
+ annotated.chomp! if missing_newline?
68
+ annotated
69
+ end
70
+ end
71
+
72
+ def exitstatus
73
+ @evaluated || raise(MustEvaluateFirst.new __method__)
74
+ record_exitstatus.exitstatus
75
+ end
76
+
77
+ private
78
+
79
+ attr_accessor :config
80
+
81
+ def missing_newline?
82
+ @missing_newline ||= !config.body.end_with?("\n")
83
+ end
84
+
85
+ def code
86
+ @code ||= Code.new(normalized_cleaned_body, config.filename)
87
+ end
88
+
89
+ def record_exitstatus
90
+ @record_exitstatus ||= SeeingIsBelieving::EventStream::Handlers::RecordExitStatus.new config.lib_options.event_handler
91
+ end
92
+
93
+ def normalized_cleaned_body
94
+ @normalized_cleaned_body ||= begin
95
+ body_with_nl = config.body
96
+ body_with_nl += "\n" if missing_newline?
97
+ RemoveAnnotations.call body_with_nl,
98
+ config.remove_value_prefixes,
99
+ config.markers
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -6,17 +6,17 @@ class SeeingIsBelieving
6
6
  # line_length is the length of the line this comment is being appended to
7
7
  #
8
8
  # For examples of what the options are, and how they all fit together, see
9
- # spec/binary/comment_formatter_spec.rb
10
- class CommentFormatter
9
+ # spec/binary/format_comment.rb
10
+ class FormatComment
11
11
  def self.call(*args)
12
12
  new(*args).call
13
13
  end
14
14
 
15
15
  def initialize(line_length, separator, result, options)
16
- self.line_length = line_length
17
- self.separator = separator
18
- self.result = result
19
- self.options = options
16
+ self.line_length = line_length
17
+ self.separator = separator
18
+ self.result = result
19
+ self.options = options
20
20
  end
21
21
 
22
22
  def call
@@ -1,21 +1,18 @@
1
- require 'seeing_is_believing/binary' # Defines the regexes to locate the markers
2
- require 'seeing_is_believing/parser_helpers' # We have to parse the file to find the comments
1
+ require 'seeing_is_believing/binary' # Defines the regexes to locate the markers
2
+ require 'seeing_is_believing/code' # We have to parse the file to find the comments
3
3
 
4
4
  class SeeingIsBelieving
5
5
  module Binary
6
- # TODO: might be here that we hit the issue where
7
- # you sometimes have to run it 2x to get it to correctly reset whitespace
8
- # should wipe out the full_range rather than just the comment_range
9
6
  class RemoveAnnotations
10
- def self.call(raw_code, should_clean_values, markers)
11
- new(raw_code, should_clean_values, markers).call
7
+ def self.call(raw_code, remove_value_prefixes, markers)
8
+ new(raw_code, remove_value_prefixes, markers).call
12
9
  end
13
10
 
14
- def initialize(raw_code, should_clean_values, markers)
15
- self.should_clean_values = should_clean_values
16
- self.raw_code = raw_code
17
- self.markers = markers # TECHNICALLY THESE ARE REGEXES RIGHT NOW
18
- self.code = Code.new(raw_code, 'strip_comments')
11
+ def initialize(raw_code, remove_value_prefixes, markers)
12
+ self.remove_value_prefixes = remove_value_prefixes
13
+ self.raw_code = raw_code
14
+ self.markers = markers
15
+ self.code = Code.new(raw_code, 'strip_comments')
19
16
  end
20
17
 
21
18
  def call
@@ -27,9 +24,13 @@ class SeeingIsBelieving
27
24
 
28
25
  case comment.text
29
26
  when value_regex
30
- next unless should_clean_values
31
- code.rewriter.remove comment.comment_range
32
- remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, false
27
+ if remove_value_prefixes
28
+ code.rewriter.remove comment.comment_range
29
+ remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, false
30
+ else
31
+ prefix = comment.text[value_regex].rstrip
32
+ code.rewriter.replace comment.comment_range, prefix
33
+ end
33
34
  when exception_regex
34
35
  code.rewriter.remove comment.comment_range
35
36
  remove_whitespace_before comment.comment_range.begin_pos, code.buffer, code.rewriter, true
@@ -49,7 +50,7 @@ class SeeingIsBelieving
49
50
 
50
51
  private
51
52
 
52
- attr_accessor :raw_code, :should_clean_values, :markers, :code
53
+ attr_accessor :raw_code, :remove_value_prefixes, :markers, :code
53
54
 
54
55
  # any whitespace before the index (on the same line) will be removed
55
56
  # if the preceding whitespace is at the beginning of the line, the newline will be removed
@@ -57,11 +58,11 @@ class SeeingIsBelieving
57
58
  def remove_whitespace_before(index, buffer, rewriter, remove_preceding_newline)
58
59
  end_pos = index
59
60
  begin_pos = end_pos - 1
60
- begin_pos -= 1 while raw_code[begin_pos] =~ /\s/ && raw_code[begin_pos] != "\n"
61
- begin_pos -= 1 if raw_code[begin_pos] == "\n"
62
- begin_pos -= 1 if raw_code[begin_pos] == "\n" && remove_preceding_newline
61
+ begin_pos -= 1 while 0 <= begin_pos && raw_code[begin_pos] =~ /\s/ && raw_code[begin_pos] != "\n"
62
+ begin_pos -= 1 if 0 <= begin_pos && raw_code[begin_pos] == "\n"
63
+ begin_pos -= 1 if remove_preceding_newline && 0 <= begin_pos && raw_code[begin_pos] == "\n"
63
64
  return if begin_pos.next == end_pos
64
- rewriter.remove Parser::Source::Range.new(buffer, begin_pos.next, end_pos)
65
+ rewriter.remove code.range_for(begin_pos.next, end_pos)
65
66
  end
66
67
 
67
68
  def annotation_chunks_in(code)
@@ -90,24 +91,24 @@ class SeeingIsBelieving
90
91
  }
91
92
  end
92
93
 
94
+ def value_prefix
95
+ @value_prefix ||= markers.fetch(:value).fetch(:prefix)
96
+ end
97
+
93
98
  def value_regex
94
- markers.fetch(:value)
99
+ @value_regex ||= markers.fetch(:value).fetch(:regex)
95
100
  end
96
101
 
97
102
  def exception_regex
98
- markers.fetch(:exception)
103
+ @exception_regex ||= markers.fetch(:exception).fetch(:regex)
99
104
  end
100
105
 
101
106
  def stdout_regex
102
- markers.fetch(:stdout)
107
+ @stdout_regex ||= markers.fetch(:stdout).fetch(:regex)
103
108
  end
104
109
 
105
110
  def stderr_regex
106
- markers.fetch(:stderr)
107
- end
108
-
109
- def nextline_regex
110
- markers.fetch(:nextline)
111
+ @stderr_regex ||= markers.fetch(:stderr).fetch(:regex)
111
112
  end
112
113
  end
113
114
  end
@@ -1,63 +1,62 @@
1
1
  require 'seeing_is_believing/code'
2
+ require 'seeing_is_believing/binary/commentable_lines'
2
3
 
3
4
  class SeeingIsBelieving
4
5
  module Binary
6
+ # can this be joined into CommentLines?
7
+ # that one yields every commentable line, this one just lines which have comments
8
+ # what they yield is a little different, too, but their algorithms and domain are very similar
5
9
  module RewriteComments
6
- def self.call(code, options={}, &mapping)
7
- code = Code.new(code)
8
- comments = code.inline_comments
9
- buffer = code.buffer
10
+ Options = HashStruct.anon do
11
+ attribute(:include_lines) { [] }
12
+ end
13
+
14
+ def self.call(raw_code, options={}, &mapping)
15
+ code = Code.new(raw_code)
16
+ comments = code.inline_comments
17
+ extra_lines = Options.new(options).include_lines
10
18
 
19
+ # update existing comments
11
20
  comments.each do |comment|
12
21
  new_whitespace, new_comment = mapping.call comment
13
22
  code.rewriter.replace comment.whitespace_range, new_whitespace
14
23
  code.rewriter.replace comment.comment_range, new_comment
15
24
  end
16
25
 
17
- line_begins = line_begins_for(buffer.source)
18
- options.fetch(:always_rewrite, []).each { |line_number|
19
- next if comments.any? { |c| c.line_number == line_number }
20
-
21
- # TODO: can this move down into Code?
22
- _, next_line_index = (line_begins.find { |ln, index| ln == line_number } || [nil, buffer.source.size.next])
23
- col = 0
24
- col += 1 until col == next_line_index || buffer.source[next_line_index-2-col] == "\n"
25
-
26
- index = next_line_index - 1
27
- range = Parser::Source::Range.new buffer, index, index
28
-
29
- comment = Code::InlineComment.new line_number, # line_number,
30
- col, # preceding_whitespace_range.column,
31
- "", # preceding_whitespace,
32
- col, # comment.location.column,
33
- "", # comment.text,
34
- range, # range_for(first_char, comment.location.expression.end_pos),
35
- range, # preceding_whitespace_range,
36
- range # comment.location.expression
26
+ # remove extra lines that are handled / uncommentable
27
+ comments.each { |c| extra_lines.delete c.line_number }
28
+ commentable_linenums = CommentableLines.call(code.raw).map { |linenum, *| linenum }
29
+ extra_lines.select! { |linenum| commentable_linenums.include? linenum }
30
+
31
+ # add additional comments
32
+ extra_lines.each do |line_number|
33
+ line_begin_col = code.linenum_to_index(line_number)
34
+ nextline_begin_col = code.linenum_to_index(line_number.next)
35
+ nextline_begin_col -= 1 if code.raw[nextline_begin_col-1] == "\n"
36
+ whitespace_col = nextline_begin_col-1
37
+ whitespace_col -= 1 while line_begin_col < whitespace_col &&
38
+ code.raw[whitespace_col] =~ /\s/
39
+ whitespace_col += 1
40
+ whitespace_range = code.range_for(whitespace_col, nextline_begin_col)
41
+ comment_range = code.range_for(nextline_begin_col, nextline_begin_col)
42
+
43
+ comment = Code::InlineComment.new \
44
+ line_number: line_number,
45
+ whitespace_col: whitespace_col-line_begin_col,
46
+ whitespace: code.raw[whitespace_col...nextline_begin_col]||"",
47
+ text_col: nextline_begin_col-line_begin_col,
48
+ text: "",
49
+ full_range: whitespace_range,
50
+ whitespace_range: whitespace_range,
51
+ comment_range: comment_range
37
52
 
38
53
  whitespace, body = mapping.call comment
39
- code.rewriter.insert_before range, "#{whitespace}#{body}"
40
- }
54
+ code.rewriter.replace whitespace_range, "#{whitespace}#{body}"
55
+ end
41
56
 
57
+ # perform the rewrite
42
58
  code.rewriter.process
43
59
  end
44
-
45
- # TODO: Move down into the Code obj?
46
- # returns: [[lineno, index], ...]
47
- def self.line_begins_for(raw_code)
48
- # Copied from here https://github.com/whitequark/parser/blob/34c40479293bb9b5ba217039cf349111466d1f9a/lib/parser/source/buffer.rb#L213-227
49
- # I figured it's better to copy it than to violate encapsulation since this is private
50
- line_begins, index = [ [ 0, 0 ] ], 1
51
-
52
- raw_code.each_char do |char|
53
- if char == "\n"
54
- line_begins.unshift [ line_begins.length, index ]
55
- end
56
-
57
- index += 1
58
- end
59
- line_begins
60
- end
61
60
  end
62
61
  end
63
62
  end
@@ -1,99 +1,155 @@
1
- require 'parser/current'
1
+ # With new versioning, there's lots of small versions
2
+ # we don't need Parser to complain that we're on 2.1.1 and its parsing 2.1.5
3
+ # https://github.com/whitequark/parser/blob/e2249d7051b1adb6979139928e14a81bc62f566e/lib/parser/current.rb#L3
4
+ class << (Parser ||= Module.new)
5
+ def warn(*) end
6
+ require 'parser/current'
7
+ remove_method :warn
8
+ end
9
+
10
+ require 'seeing_is_believing/hash_struct'
11
+
2
12
  class SeeingIsBelieving
3
13
  class Code
4
- InlineComment = Struct.new :line_number,
5
- :whitespace_col,
6
- :whitespace,
7
- :text_col,
8
- :text,
9
- :full_range,
10
- :whitespace_range,
11
- :comment_range
12
-
13
- # At prsent, it is expected that the syntax is validated before code arrives here
14
- # or that its validity doesn't matter (e.g. extracting comments)
15
- def initialize(raw_ruby_code, name="SeeingIsBelieving")
16
- self.code = raw_ruby_code
17
- self.name = name
14
+ InlineComment = HashStruct.for :line_number, :whitespace_col, :whitespace, :text_col, :text, :full_range, :whitespace_range, :comment_range
15
+ Syntax = HashStruct.for error_message: nil, line_number: nil do
16
+ def valid?() !error_message end
17
+ def invalid?() !valid? end
18
18
  end
19
19
 
20
- def buffer() @buffer ||= (parse && @buffer ) end
21
- def parser() @parser ||= (parse && @parser ) end
22
- def rewriter() @rewriter ||= (parse && @rewriter ) end
23
- def inline_comments() @comments ||= (parse && @inline_comments) end
24
- def root() @root ||= (parse && @root ) end
20
+ attr_reader :raw, :buffer, :parser, :rewriter, :inline_comments, :root, :raw_comments, :syntax, :body_range
21
+
22
+ def initialize(raw_code, name=nil)
23
+ raw_code[-1] == "\n" || raise(SyntaxError, "Code must end in a newline for the sake of consistency (sanity)")
24
+ @raw = raw_code
25
+ @buffer = Parser::Source::Buffer.new(name||"SeeingIsBelieving")
26
+ @buffer.source = raw
27
+ builder = Parser::Builders::Default.new.tap { |b| b.emit_file_line_as_literals = false }
28
+ @rewriter = Parser::Source::Rewriter.new buffer
29
+ @raw_comments, tokens = comments_and_tokens(builder, buffer)
30
+ @body_range = body_range_from_tokens(tokens)
31
+ @parser = Parser::CurrentRuby.new builder
32
+ @inline_comments = raw_comments.select(&:inline?).map { |c| wrap_comment c }
33
+ begin
34
+ @root = @parser.parse(@buffer)
35
+ @syntax = Syntax.new
36
+ rescue Parser::SyntaxError
37
+ @syntax = Syntax.new error_message: $!.message, line_number: index_to_linenum($!.diagnostic.location.begin_pos)
38
+ ensure
39
+ @root ||= null_node
40
+ end
41
+ end
25
42
 
26
43
  def range_for(start_index, end_index)
27
44
  Parser::Source::Range.new buffer, start_index, end_index
28
45
  end
29
46
 
30
- private
31
-
32
- attr_accessor :code, :name
47
+ def index_to_linenum(char_index)
48
+ line_indexes.index { |line_index| char_index < line_index } || line_indexes.size
49
+ end
33
50
 
34
- def parse
35
- @buffer = Parser::Source::Buffer.new(name)
36
- @buffer.source = code
37
- builder = Parser::Builders::Default.new
38
- builder.emit_file_line_as_literals = false # should be injectible?
39
- @parser = Parser::CurrentRuby.new builder
40
- @rewriter = Parser::Source::Rewriter.new @buffer
51
+ def linenum_to_index(line_num)
52
+ return raw.size if line_indexes.size < line_num
53
+ line_indexes[line_num - 1]
54
+ end
41
55
 
42
- can_parse_invalid_code(@parser)
56
+ def heredoc?(ast)
57
+ # some strings are fucking weird.
58
+ # e.g. the "1" in `%w[1]` returns nil for ast.location.begin
59
+ # and `__FILE__` is a string whose location is a Parser::Source::Map instead of a Parser::Source::Map::Collection,
60
+ # so it has no #begin
61
+ ast.kind_of?(Parser::AST::Node) &&
62
+ (ast.type == :dstr || ast.type == :str) &&
63
+ (location = ast.location) &&
64
+ (location.kind_of? Parser::Source::Map::Heredoc)
65
+ end
43
66
 
44
- @root, all_comments, tokens = parser.tokenize(@buffer)
67
+ def void_value?(ast)
68
+ case ast && ast.type
69
+ when :begin, :kwbegin, :resbody
70
+ void_value?(ast.children.last)
71
+ when :rescue, :ensure
72
+ ast.children.any? { |child| void_value? child }
73
+ when :if
74
+ void_value?(ast.children[1]) || void_value?(ast.children[2])
75
+ when :return, :next, :redo, :retry, :break
76
+ true
77
+ else
78
+ false
79
+ end
80
+ end
45
81
 
46
- @inline_comments = all_comments.select(&:inline?).map { |c| wrap_comment c }
82
+ def line_indexes
83
+ @line_indexes ||= [ 0,
84
+ *raw.each_char
85
+ .with_index(1)
86
+ .select { |char, index| char == "\n" }
87
+ .map { |char, index| index },
88
+ ].freeze
47
89
  end
48
90
 
49
- def can_parse_invalid_code(parser)
50
- # THIS IS SO WE CAN EXTRACT COMMENTS FROM INVALID FILES.
91
+ private
51
92
 
93
+ def comments_and_tokens(builder, buffer)
94
+ # THIS IS SO WE CAN EXTRACT COMMENTS FROM INVALID FILES.
52
95
  # We do it by telling Parser's diagnostic to not blow up.
53
96
  # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/diagnostic/engine.rb
54
-
55
97
  # However, this probably implies SiB won't work on Rbx/JRuby
56
98
  # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/base.rb#L129-134
57
-
58
99
  # Ideally we could just do this
59
100
  # parser.diagnostics.all_errors_are_fatal = false
60
101
  # parser.diagnostics.ignore_warnings = false
61
-
62
102
  # But, the parser will still blow up on "fatal" errors (e.g. unterminated string) So we need to actually change it.
63
103
  # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/diagnostic/engine.rb#L99
64
-
65
104
  # We could make a NullDiagnostics like this:
66
105
  # class NullDiagnostics < Parser::Diagnostic::Engine
67
106
  # def process(*)
68
107
  # # no op
69
108
  # end
70
109
  # end
71
-
72
110
  # But we don't control initialization of the variable, and the value gets passed around, at least into the lexer.
73
111
  # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/base.rb#L139
74
112
  # and since it's all private, it could change at any time (Parser is very state based),
75
113
  # so I think it's just generally safer to mutate that one object, as we do now.
114
+ parser = Parser::CurrentRuby.new builder
76
115
  diagnostics = parser.diagnostics
77
116
  def diagnostics.process(*)
78
117
  self
79
118
  end
119
+ _, all_comments, tokens = parser.tokenize(@buffer)
120
+ [all_comments, tokens]
121
+ end
122
+
123
+ def body_range_from_tokens(tokens)
124
+ return range_for(0, 0) if raw.start_with? "__END__\n"
125
+ (name, (_, range)) = tokens.max_by { |name, (data, range)| range.end_pos } ||
126
+ [nil, [nil, range_for(0, 1)]]
127
+ end_pos = range.end_pos
128
+ end_pos += 1 if name == :tCOMMENT || name == :tSTRING_END #
129
+ range_for 0, end_pos
80
130
  end
81
131
 
82
132
  def wrap_comment(comment)
83
133
  last_char = comment.location.expression.begin_pos
84
134
  first_char = last_char
85
- first_char -= 1 while first_char > 0 && code[first_char-1] =~ /[ \t]/
135
+ first_char -= 1 while first_char > 0 && raw[first_char-1] =~ /[ \t]/
86
136
  preceding_whitespace = buffer.source[first_char...last_char]
87
137
  preceding_whitespace_range = range_for first_char, last_char
88
138
 
89
- InlineComment.new comment.location.line,
90
- preceding_whitespace_range.column,
91
- preceding_whitespace,
92
- comment.location.column,
93
- comment.text,
94
- range_for(first_char, comment.location.expression.end_pos),
95
- preceding_whitespace_range,
96
- comment.location.expression
139
+ InlineComment.new line_number: comment.location.line,
140
+ whitespace_col: preceding_whitespace_range.column,
141
+ whitespace: preceding_whitespace,
142
+ text_col: comment.location.column,
143
+ text: comment.text,
144
+ full_range: range_for(first_char, comment.location.expression.end_pos),
145
+ whitespace_range: preceding_whitespace_range,
146
+ comment_range: comment.location.expression
97
147
  end
148
+
149
+ def null_node
150
+ location = Parser::Source::Map::Collection.new nil, nil, range_for(0, 0)
151
+ Parser::AST::Node.new :null_node, [], location: location
152
+ end
153
+
98
154
  end
99
155
  end