seeing_is_believing 2.0.0.beta1 → 2.0.0.beta2

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