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

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