seeing_is_believing 2.0.0.beta1 → 2.0.0.beta2

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/Readme.md +8 -20
  3. data/features/examples.feature +1 -3
  4. data/features/flags.feature +4 -12
  5. data/features/regression.feature +95 -1
  6. data/lib/seeing_is_believing.rb +17 -21
  7. data/lib/seeing_is_believing/binary.rb +3 -3
  8. data/lib/seeing_is_believing/binary/add_annotations.rb +86 -118
  9. data/lib/seeing_is_believing/binary/align_chunk.rb +22 -23
  10. data/lib/seeing_is_believing/binary/align_file.rb +10 -13
  11. data/lib/seeing_is_believing/binary/align_line.rb +0 -4
  12. data/lib/seeing_is_believing/binary/arg_parser.rb +11 -11
  13. data/lib/seeing_is_believing/binary/clean_body.rb +96 -0
  14. data/lib/seeing_is_believing/binary/comment_formatter.rb +55 -0
  15. data/lib/seeing_is_believing/binary/comment_lines.rb +37 -0
  16. data/lib/seeing_is_believing/binary/commentable_lines.rb +158 -0
  17. data/lib/seeing_is_believing/binary/rewrite_comments.rb +42 -0
  18. data/lib/seeing_is_believing/result.rb +2 -9
  19. data/lib/seeing_is_believing/the_matrix.rb +8 -8
  20. data/lib/seeing_is_believing/version.rb +1 -1
  21. data/lib/seeing_is_believing/{program_rewriter.rb → wrap_expressions.rb} +30 -22
  22. data/spec/binary/arg_parser_spec.rb +7 -7
  23. data/spec/binary/clean_body_spec.rb +217 -0
  24. data/spec/binary/comment_formatter_spec.rb +54 -0
  25. data/spec/binary/comment_lines_spec.rb +847 -0
  26. data/spec/binary/rewrite_comments_spec.rb +54 -0
  27. data/spec/seeing_is_believing_spec.rb +1 -2
  28. data/spec/{program_rewriter_spec.rb → wrap_expressions_spec.rb} +117 -40
  29. metadata +41 -56
  30. data/lib/seeing_is_believing/binary/line_formatter.rb +0 -47
  31. data/lib/seeing_is_believing/binary/remove_previous_annotations.rb +0 -75
  32. data/lib/seeing_is_believing/queue.rb +0 -55
  33. data/lib/seeing_is_believing/remove_inline_comments.rb +0 -46
  34. data/lib/seeing_is_believing/syntax_analyzer.rb +0 -61
  35. data/lib/seeing_is_believing/tracks_line_numbers_seen.rb +0 -19
  36. data/spec/binary/line_formatter_spec.rb +0 -53
  37. data/spec/binary/remove_previous_annotations_spec.rb +0 -198
  38. data/spec/queue_spec.rb +0 -80
  39. data/spec/syntax_analyzer_spec.rb +0 -56
@@ -1,38 +1,37 @@
1
+ require 'seeing_is_believing/binary/commentable_lines'
2
+
1
3
  class SeeingIsBelieving
2
4
  class Binary
3
5
  class AlignChunk
4
- attr_accessor :body, :start_line, :end_line
5
-
6
6
  def initialize(body, start_line, end_line)
7
7
  self.body, self.start_line, self.end_line = body, start_line, end_line
8
8
  end
9
9
 
10
10
  # max line length of the the chunk (newline separated sections of code exempting comments) + 2 spaces for padding
11
11
  def line_length_for(line_number)
12
- line_lengths[line_number]
12
+ line_lengths.fetch line_number, 0
13
13
  end
14
14
 
15
- def line_lengths
16
- @line_lengths ||= Hash[
17
- body.each_line
18
- .map(&:chomp)
19
- .map.with_index(1) { |line, index| [line, index] }
20
- .take_while { |line, index| not start_of_data_segment? line }
21
- .select { |line, index| not SyntaxAnalyzer.begins_multiline_comment?(line) .. SyntaxAnalyzer.ends_multiline_comment?(line ) }
22
- .reject { |line, index| SyntaxAnalyzer.ends_in_comment? line }
23
- .slice_before { |line, index| line == '' }
24
- .map { |slice|
25
- max_chunk_length = 2 + slice.select { |line, index| start_line <= index && index <= end_line }
26
- .map { |line, index| line.length }
27
- .max
28
- slice.map { |line, index| [index, max_chunk_length] }
29
- }
30
- .flatten(1)
31
- ]
32
- end
15
+ private
16
+
17
+ attr_accessor :body, :start_line, :end_line
33
18
 
34
- def start_of_data_segment?(line)
35
- SyntaxAnalyzer.begins_data_segment?(line.chomp)
19
+ def line_lengths
20
+ @line_lengths ||= begin
21
+ line_num_to_indexes = CommentableLines.new(body).call # {line_number => [index_in_file, index_in_col]}
22
+ Hash[line_num_to_indexes
23
+ .keys
24
+ .sort
25
+ .slice_before { |line_number| line_num_to_indexes[line_number].last.zero? }
26
+ .map { |slice|
27
+ max_chunk_length = 2 + slice.select { |line_num| start_line <= line_num && line_num <= end_line }
28
+ .map { |line_num| line_num_to_indexes[line_num].last }
29
+ .max
30
+ slice.map { |line_number| [line_number, max_chunk_length] }
31
+ }
32
+ .flatten(1)
33
+ ]
34
+ end
36
35
  end
37
36
  end
38
37
  end
@@ -1,3 +1,5 @@
1
+ require 'seeing_is_believing/binary/commentable_lines'
2
+
1
3
  class SeeingIsBelieving
2
4
  class Binary
3
5
  class AlignFile
@@ -9,19 +11,14 @@ class SeeingIsBelieving
9
11
 
10
12
  # max line length of the lines to output (exempting comments) + 2 spaces for padding
11
13
  def line_length_for(line_number)
12
- @max_source_line_length ||= 2 + body.each_line
13
- .map(&:chomp)
14
- .select.with_index(1) { |line, index| start_line <= index && index <= end_line }
15
- .take_while { |line| not start_of_data_segment? line }
16
- .select { |line| not SyntaxAnalyzer.begins_multiline_comment?(line) .. SyntaxAnalyzer.ends_multiline_comment?(line ) }
17
- .reject { |line| SyntaxAnalyzer.ends_in_comment? line }
18
- .map(&:length)
19
- .concat([0])
20
- .max
21
- end
22
-
23
- def start_of_data_segment?(line)
24
- SyntaxAnalyzer.begins_data_segment?(line.chomp)
14
+ @max_source_line_length ||= 2 + begin
15
+ line_num_to_indexes = CommentableLines.new(body).call # {line_number => [index_in_file, index_in_col]}
16
+ max_value = line_num_to_indexes
17
+ .select { |line_num, _| start_line <= line_num && line_num <= end_line }
18
+ .values
19
+ .map { |index, col| col }.max
20
+ max_value || 0
21
+ end
25
22
  end
26
23
  end
27
24
  end
@@ -20,10 +20,6 @@ class SeeingIsBelieving
20
20
  .map { |line, index| [index, line.length+2] }
21
21
  ]
22
22
  end
23
-
24
- def start_of_data_segment?(line)
25
- SyntaxAnalyzer.begins_data_segment?(line.chomp)
26
- end
27
23
  end
28
24
  end
29
25
  end
@@ -29,15 +29,15 @@ class SeeingIsBelieving
29
29
  when '-x', '--xmpfilter-style' then options[:xmpfilter_style] = true
30
30
  when '-i', '--inherit-exit-status' then options[:inherit_exit_status] = true
31
31
  when '-g', '--debug' then options[:debugger] = Debugger.new(enabled: true, colour: true)
32
- when '-l', '--start-line' then extract_positive_int_for :start_line, arg
33
- when '-L', '--end-line' then extract_positive_int_for :end_line, arg
34
- when '-d', '--line-length' then extract_positive_int_for :line_length, arg
35
- when '-D', '--result-length' then extract_positive_int_for :result_length, arg
36
- when '-t', '--timeout' then extract_non_negative_float_for :timeout, arg
37
- when '-r', '--require' then next_arg("#{arg} expected a filename as the following argument but did not see one") { |filename| options[:require] << filename }
38
- when '-I', '--load-path' then next_arg("#{arg} expected a directory as the following argument but did not see one") { |dir| options[:load_path] << dir }
39
- when '-e', '--program' then next_arg("#{arg} expected a program as the following argument but did not see one") { |program| options[:program] = program }
40
- when '-a', '--as' then next_arg("#{arg} expected a filename as the following argument but did not see one") { |filename| options[:as] = filename }
32
+ when '-l', '--start-line' then extract_positive_int_for :start_line, arg
33
+ when '-L', '--end-line' then extract_positive_int_for :end_line, arg
34
+ when '-d', '--line-length' then extract_positive_int_for :max_line_length, arg
35
+ when '-D', '--result-length' then extract_positive_int_for :max_result_length, arg
36
+ when '-t', '--timeout' then extract_non_negative_float_for :timeout, arg
37
+ when '-r', '--require' then next_arg("#{arg} expected a filename as the following argument but did not see one") { |filename| options[:require] << filename }
38
+ when '-I', '--load-path' then next_arg("#{arg} expected a directory as the following argument but did not see one") { |dir| options[:load_path] << dir }
39
+ when '-e', '--program' then next_arg("#{arg} expected a program as the following argument but did not see one") { |program| options[:program] = program }
40
+ when '-a', '--as' then next_arg("#{arg} expected a filename as the following argument but did not see one") { |filename| options[:as] = filename }
41
41
  when '-s', '--alignment-strategy' then extract_alignment_strategy
42
42
  when /\A-K(.+)/ then options[:encoding] = $1
43
43
  when '-K', '--encoding' then next_arg("#{arg} expects an encoding, see `man ruby` for possibile values") { |encoding| options[:encoding] = encoding }
@@ -78,9 +78,9 @@ class SeeingIsBelieving
78
78
  program: nil,
79
79
  filename: nil,
80
80
  start_line: 1,
81
- line_length: Float::INFINITY,
82
81
  end_line: Float::INFINITY,
83
- result_length: Float::INFINITY,
82
+ max_line_length: Float::INFINITY,
83
+ max_result_length: Float::INFINITY,
84
84
  timeout: 0, # timeout lib treats this as infinity
85
85
  errors: [],
86
86
  require: [],
@@ -0,0 +1,96 @@
1
+ # CleanedBody
2
+ # takes a body
3
+ # removes annotations
4
+ # only removes "# =>" when should_clean_values is false
5
+
6
+ require 'parser/current'
7
+
8
+ class SeeingIsBelieving
9
+ class Binary
10
+ class CleanBody
11
+ def self.call(code, should_clean_values)
12
+ new(code, should_clean_values).call
13
+ end
14
+
15
+ def initialize(code, should_clean_values)
16
+ self.should_clean_values = should_clean_values
17
+ self.code = code
18
+ end
19
+
20
+ def call
21
+ buffer = Parser::Source::Buffer.new "strip_comments"
22
+ buffer.source = code
23
+ parser = Parser::CurrentRuby.new
24
+ rewriter = Parser::Source::Rewriter.new(buffer)
25
+ ast, comments = parser.parse_with_comments(buffer)
26
+ removed_comments = { result: [], exception: [], stdout: [], stderr: [] }
27
+
28
+ comments.each do |comment|
29
+ case comment.text
30
+ when /\A#\s*=>/
31
+ if should_clean_values
32
+ removed_comments[:result] << comment
33
+ rewriter.remove comment.location.expression
34
+ end
35
+ when /\A#\s*~>/
36
+ removed_comments[:exception] << comment
37
+ rewriter.remove comment.location.expression
38
+ when /\A#\s*>>/ then
39
+ removed_comments[:stdout] << comment
40
+ rewriter.remove comment.location.expression
41
+ when /\A#\s*!>/ then
42
+ removed_comments[:stderr] << comment
43
+ rewriter.remove comment.location.expression
44
+ end
45
+ end
46
+
47
+ remove_whitespace_preceeding_comments(buffer, rewriter, removed_comments)
48
+ rewriter.process
49
+ rescue Parser::SyntaxError => e
50
+ raise SyntaxError, e.message
51
+ end
52
+
53
+ private
54
+
55
+ attr_accessor :code, :should_clean_values, :buffer
56
+
57
+ def remove_whitespace_preceeding_comments(buffer, rewriter, removed_comments)
58
+ removed_comments[:result].each { |comment| remove_whitespace_before comment.location.expression.begin_pos, buffer, rewriter, false }
59
+ removed_comments[:exception].each { |comment| remove_whitespace_before comment.location.expression.begin_pos, buffer, rewriter, true }
60
+ removed_comments[:stdout].each { |comment| remove_whitespace_before comment.location.expression.begin_pos, buffer, rewriter, true }
61
+ removed_comments[:stderr].each { |comment| remove_whitespace_before comment.location.expression.begin_pos, buffer, rewriter, true }
62
+ end
63
+
64
+ # any whitespace before the index (on the same line) will be removed
65
+ # if the preceeding whitespace is at the beginning of the line, the newline will be removed
66
+ # if there is a newline before all of that, and remove_preceeding_newline is true, it will be removed as well
67
+ def remove_whitespace_before(index, buffer, rewriter, remove_preceeding_newline)
68
+ end_pos = index
69
+ begin_pos = end_pos - 1
70
+ begin_pos -= 1 while code[begin_pos] =~ /\s/ && code[begin_pos] != "\n"
71
+ begin_pos -= 1 if code[begin_pos] == "\n"
72
+ begin_pos -= 1 if code[begin_pos] == "\n" && remove_preceeding_newline
73
+ return if begin_pos.next == end_pos
74
+ rewriter.remove Parser::Source::Range.new(buffer, begin_pos.next, end_pos)
75
+ end
76
+
77
+ # returns comments in groups that are on consecutive lines
78
+ def adjacent_comments(comments, buffer)
79
+ comments = comments.sort_by { |comment| comment.location.begin_pos }
80
+ current_chunk = 0
81
+ last_line_seen = -100
82
+ chunks_to_comment = comments.chunk do |comment|
83
+ line, col = buffer.decompose_position comment.location.begin_pos
84
+ if last_line_seen.next == line
85
+ last_line_seen = line
86
+ current_chunk
87
+ else
88
+ last_line_seen = line
89
+ current_chunk += 1
90
+ end
91
+ end
92
+ chunks_to_comment.map &:last
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,55 @@
1
+ class SeeingIsBelieving
2
+ class Binary
3
+ class CommentFormatter
4
+ def self.call(*args)
5
+ new(*args).call
6
+ end
7
+
8
+ def initialize(line_length, separator, result, options)
9
+ self.line_length = line_length
10
+ self.separator = separator
11
+ self.result = result.gsub "\n", '\n'
12
+ self.options = options
13
+ end
14
+
15
+ def call
16
+ @formatted ||= begin
17
+ formatted = truncate "#{separator}#{result}", max_result_length
18
+ formatted = "#{' '*padding_length}#{formatted}"
19
+ formatted = truncate formatted, max_line_length
20
+ formatted = '' unless formatted.sub(/^ */, '').start_with? separator
21
+ formatted
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_accessor :line_length, :separator, :result, :options
28
+
29
+ def max_line_length
30
+ length = options.fetch(:max_line_length, Float::INFINITY) - line_length
31
+ length = 0 if length < 0
32
+ length
33
+ end
34
+
35
+ def max_result_length
36
+ options.fetch :max_result_length, Float::INFINITY
37
+ end
38
+
39
+ def padding_length
40
+ padding_length = options.fetch(:pad_to, 0) - line_length
41
+ padding_length = 0 if padding_length < 0
42
+ padding_length
43
+ end
44
+
45
+ def truncate(string, length)
46
+ return string if string.size <= length
47
+ ellipsify string.slice(0, length)
48
+ end
49
+
50
+ def ellipsify(string)
51
+ string.sub(/.{0,3}$/) { |last_chars| last_chars.gsub /./, '.' }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ require 'seeing_is_believing/binary/commentable_lines'
2
+
3
+ class SeeingIsBelieving
4
+ class Binary
5
+
6
+ # takes a body and a block
7
+ # passes the block the line
8
+ # the block returns the comment to add at the end of it
9
+ class CommentLines
10
+ def self.call(code, &commenter)
11
+ new(code, &commenter).call
12
+ end
13
+
14
+ def initialize(code, &commenter)
15
+ self.code, self.commenter = code, commenter
16
+ end
17
+
18
+ def call
19
+ @call ||= begin
20
+ commentable_lines = CommentableLines.new code
21
+ commentable_lines.call.each do |line_number, (index_of_newline, col)|
22
+ first_index = last_index = index_of_newline
23
+ first_index -= 1 while first_index > 0 && code[first_index-1] != "\n"
24
+ comment_text = commenter.call code[first_index...last_index], line_number
25
+ range = Parser::Source::Range.new(commentable_lines.buffer, first_index, last_index)
26
+ commentable_lines.rewriter.insert_after range, comment_text
27
+ end
28
+ commentable_lines.rewriter.process
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ attr_accessor :code, :commenter
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,158 @@
1
+ require 'parser/current'
2
+
3
+ class SeeingIsBelieving
4
+ class Binary
5
+
6
+ class CommentableLines
7
+ def self.call(code)
8
+ new(code).call
9
+ end
10
+
11
+ def initialize(code)
12
+ self.code = code
13
+ end
14
+
15
+ def call
16
+ @call ||= begin
17
+ line_num_to_indexes = line_nums_to_last_index_and_col(buffer)
18
+ remove_lines_after_data_segment line_num_to_indexes
19
+ remove_lines_whose_newline_is_escaped line_num_to_indexes
20
+ remove_lines_ending_in_comments line_num_to_indexes, comments
21
+ remove_lines_inside_of_strings_and_things line_num_to_indexes, root
22
+ line_num_to_indexes
23
+ end
24
+ end
25
+
26
+ def buffer
27
+ @buffer ||= Parser::Source::Buffer.new("strip_comments").tap { |b| b.source = code }
28
+ end
29
+
30
+ def rewriter
31
+ @rewriter ||= Parser::Source::Rewriter.new(buffer)
32
+ end
33
+
34
+ private
35
+
36
+ attr_accessor :code
37
+
38
+ def parser
39
+ @parser ||= Parser::CurrentRuby.new
40
+ end
41
+
42
+ def root
43
+ parse!
44
+ @root
45
+ end
46
+
47
+ def comments
48
+ parse!
49
+ @comments
50
+ end
51
+
52
+
53
+ def parse!
54
+ return if @root
55
+ @root, @comments = parser.parse_with_comments(buffer)
56
+ end
57
+
58
+ def line_nums_to_last_index_and_col(buffer)
59
+ line_num_to_indexes = code.each_char
60
+ .with_index
61
+ .select { |char, index| char == "\n" } # <-- is this okay? what about other OSes?
62
+ .each_with_object(Hash.new) do |(_, index), hash|
63
+ line, col = buffer.decompose_position index
64
+ hash[line] = [index, col]
65
+ end
66
+ if code[code.size-1] != "\n" # account for the fact that the last line wouldn't have been found above if it doesn't end in a newline
67
+ line, col = buffer.decompose_position code.size
68
+ line_num_to_indexes[line] = [code.size, col]
69
+ end
70
+ line_num_to_indexes
71
+ end
72
+
73
+ def remove_lines_whose_newline_is_escaped(line_num_to_indexes)
74
+ line_num_to_indexes.select { |line_number, (index_of_newline, col)| code[index_of_newline-1] == '\\' }
75
+ .each { |line_number, (index_of_newline, col)| line_num_to_indexes.delete line_number }
76
+ end
77
+
78
+ def remove_lines_ending_in_comments(line_num_to_indexes, comments)
79
+ comments.each do |comment|
80
+ if comment.type == :inline
81
+ line_num_to_indexes.delete comment.location.line
82
+ else
83
+ begin_pos = comment.location.expression.begin_pos
84
+ end_pos = comment.location.expression.end_pos
85
+ range = begin_pos...end_pos
86
+ line_num_to_indexes.select { |line_number, (index_of_newline, col)| range.include? index_of_newline }
87
+ .each { |line_number, (index_of_newline, col)| line_num_to_indexes.delete line_number }
88
+ end
89
+ end
90
+ end
91
+
92
+ def remove_lines_inside_of_strings_and_things(line_num_to_indexes, ast)
93
+ invalid_boundaries = ranges_of_atomic_expressions ast, []
94
+ invalid_boundaries.each do |invalid_boundary|
95
+ line_num_to_indexes.select { |line_number, (index_of_newline, col)| invalid_boundary.include? index_of_newline }
96
+ .each { |line_number, (index_of_newline, col)| line_num_to_indexes.delete line_number }
97
+ end
98
+ end
99
+
100
+ def ranges_of_atomic_expressions(ast, found_ranges)
101
+ return found_ranges unless ast.kind_of? ::AST::Node
102
+ if no_comment_zone?(ast) && heredoc?(ast)
103
+ begin_pos = ast.location.expression.begin.begin_pos
104
+ begin_pos += (ast.location.expression.source =~ /\n/).next
105
+ end_pos = ast.location.expression.end.end_pos.next
106
+ found_ranges << (begin_pos...end_pos)
107
+ elsif no_comment_zone? ast
108
+ begin_pos = ast.location.expression.begin.begin_pos
109
+ end_pos = ast.location.expression.end.end_pos
110
+ found_ranges << (begin_pos...end_pos)
111
+ else
112
+ ast.children.each { |child| ranges_of_atomic_expressions child, found_ranges }
113
+ end
114
+ found_ranges
115
+ end
116
+
117
+ def no_comment_zone?(ast)
118
+ case ast.type
119
+ when :dstr, :str, :xstr, :regexp
120
+ true
121
+ when :array
122
+ the_begin = ast.location.begin
123
+ the_begin && the_begin.source =~ /\A%/
124
+ else
125
+ false
126
+ end
127
+ end
128
+
129
+ # copy/pasted from wrap_expressions
130
+ def heredoc?(ast)
131
+ # some strings are fucking weird.
132
+ # e.g. the "1" in `%w[1]` returns nil for ast.location.begin
133
+ # and `__FILE__` is a string whose location is a Parser::Source::Map instead of a Parser::Source::Map::Collection, so it has no #begin
134
+ ast.kind_of?(Parser::AST::Node) &&
135
+ (ast.type == :dstr || ast.type == :str) &&
136
+ (location = ast.location) &&
137
+ (location.respond_to?(:begin)) &&
138
+ (the_begin = location.begin) &&
139
+ (the_begin.source =~ /^\<\<-?/)
140
+ end
141
+
142
+ def remove_lines_after_data_segment(line_num_to_indexes)
143
+ data_segment_line, _ = line_num_to_indexes.find do |line_number, (end_index, col)|
144
+ if end_index == 7
145
+ code.start_with? '__END__'
146
+ elsif end_index < 7
147
+ false
148
+ else
149
+ code[(end_index-8)...end_index] == "\n__END__"
150
+ end
151
+ end
152
+ return unless data_segment_line
153
+ max_line = line_num_to_indexes.keys.max
154
+ data_segment_line.upto(max_line) { |line_number| line_num_to_indexes.delete line_number }
155
+ end
156
+ end
157
+ end
158
+ end