parser 2.0.0.pre8 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 784e6ea2b211d0e66b2c9b557ac575c2f7204444
4
- data.tar.gz: 381fae65cbb0abd8400e382c8bf926221f74929f
3
+ metadata.gz: bf0b6a5c97a393fd82aae713e9a10d4aa2108a0f
4
+ data.tar.gz: cf126c874ffc8d2430ccec47583b9dfca2afc7a1
5
5
  SHA512:
6
- metadata.gz: 720ec8f90cb16e1e38ddd02006afe4dda5ca2c9ccf31087a0906cafd406ba6865e57d5545be81f64dd17cc710c33d108e73476742475264147049d034c97c6c4
7
- data.tar.gz: f0b89adbfa47c7ba825c9d54df1d32cb0c81dec28866caea61baebd77290eb860317f3e357c10f4953056b9b1339085d0b9f2bd418848de46053b961044479c0
6
+ metadata.gz: 07a91a9884e3f0a22010c7cf60089de7b4bd4e0285df85771b9f0b52317d3b6930ba6bea44fa57c4c168723a92a3ccf4602b354a9e9e55ed7461e1009fa8431b
7
+ data.tar.gz: c95ab5caef72d3c0f936def4dd0dcb28298fbc335c9558d9d642edcfb9398a7f2b6479983cd5406ca62512f07c864b1884f4d0269bb34fb7fe75182a5086984c
@@ -1,6 +1,20 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ v2.0.0 (2013-10-06)
5
+ -------------------
6
+
7
+ API modifications:
8
+ * Source::Rewriter: raise an exception if updates clobber each other. (Peter Zotov)
9
+ * Source::Range#inspect: use full class name. (Peter Zotov)
10
+ * lexer.rl: make EOF tokens actually pointing at EOF and zero-length. (Peter Zotov)
11
+ * Source::Range#column_range: raise RangeError if range spans >1 line. (Peter Zotov)
12
+ * Source::Comment::Associator: fix argument order. (Peter Zotov)
13
+
14
+ Features implemented:
15
+ * Source::Comment: implement #inspect. (Peter Zotov)
16
+ * Backport Array#bsearch from Ruby 2.0. (Peter Zotov)
17
+
4
18
  v2.0.0.pre8 (2013-09-15)
5
19
  ------------------------
6
20
 
data/README.md CHANGED
@@ -31,13 +31,14 @@ Parse a chunk of code:
31
31
  Access the AST's source map:
32
32
 
33
33
  p Parser::CurrentRuby.parse("2 + 2").loc
34
- # #<Parser::Source::Map::Send:0x007fe0ca8a69b8
35
- # @begin=nil,
36
- # @end=nil,
37
- # @expression=#<Source::Range (string) 0...5>,
38
- # @selector=#<Source::Range (string) 2...3>>
39
-
40
- p Parser::CurrentRuby.parse("2 + 2").loc.selector.to_source
34
+ # #<Parser::Source::Map::Send:0x007fe5a1ac2388
35
+ # @dot=nil,
36
+ # @begin=nil,
37
+ # @end=nil,
38
+ # @selector=#<Source::Range (string) 2...3>,
39
+ # @expression=#<Source::Range (string) 0...5>>
40
+
41
+ p Parser::CurrentRuby.parse("2 + 2").loc.selector.source
41
42
  # "+"
42
43
 
43
44
  Parse a chunk of code and display all diagnostics:
@@ -138,6 +138,8 @@ Format:
138
138
  (array (int 1) (int 2))
139
139
 
140
140
  "[1, 2]"
141
+ ^ begin
142
+ ^ end
141
143
  ~~~~~~ expression
142
144
  ~~~
143
145
 
@@ -7,6 +7,10 @@ if RUBY_VERSION < '1.9'
7
7
  require 'parser/compatibility/ruby1_8'
8
8
  end
9
9
 
10
+ if RUBY_VERSION < '2.0'
11
+ require 'parser/compatibility/ruby1_9'
12
+ end
13
+
10
14
  ##
11
15
  # @api public
12
16
  #
@@ -182,6 +182,7 @@ module Parser
182
182
  # Ruby source code.
183
183
  #
184
184
  # @see #parse
185
+ # @see Parser::Source::Comment#associate
185
186
  # @return [Array]
186
187
  #
187
188
  def parse_with_comments(source_buffer)
@@ -217,7 +218,7 @@ module Parser
217
218
 
218
219
  ##
219
220
  # @api private
220
- # @return [TrueClass|FalseClass]
221
+ # @return [Boolean]
221
222
  #
222
223
  def in_def?
223
224
  @def_level > 0
@@ -17,7 +17,7 @@ module Parser
17
17
  #
18
18
  # Source maps are identical in both cases.
19
19
  #
20
- # @return [TrueClass|FalseClass]
20
+ # @return [Boolean]
21
21
  attr_accessor :emit_file_line_as_literals
22
22
 
23
23
  ##
@@ -0,0 +1,32 @@
1
+ unless Array.method_defined? :bsearch
2
+ class Array
3
+ def bsearch
4
+ return to_enum(__method__) unless block_given?
5
+ from = 0
6
+ to = size - 1
7
+ satisfied = nil
8
+ while from <= to
9
+ midpoint = (from + to).div(2)
10
+ result = yield(cur = self[midpoint])
11
+ case result
12
+ when Numeric
13
+ return cur if result == 0
14
+ result = result < 0
15
+ when true
16
+ satisfied = cur
17
+ when nil, false
18
+ # nothing to do
19
+ else
20
+ fail TypeError, "wrong argument type #{result.class} (must be numeric, true, false or nil)"
21
+ end
22
+
23
+ if result
24
+ to = midpoint - 1
25
+ else
26
+ from = midpoint + 1
27
+ end
28
+ end
29
+ satisfied
30
+ end
31
+ end
32
+ end
@@ -25,11 +25,11 @@ module Parser
25
25
  # @!attribute [rw] all_errors_are_fatal
26
26
  # When set to `true` any error that is encountered will result in
27
27
  # {Parser::SyntaxError} being raised.
28
- # @return [TrueClass|FalseClass]
28
+ # @return [Boolean]
29
29
  #
30
30
  # @!attribute [rw] ignore_warnings
31
31
  # When set to `true` warnings will be ignored.
32
- # @return [TrueClass|FalseClass]
32
+ # @return [Boolean]
33
33
  #
34
34
  class Diagnostic::Engine
35
35
  attr_accessor :consumer
@@ -79,7 +79,7 @@ module Parser
79
79
  # Checks whether `diagnostic` should be ignored.
80
80
  #
81
81
  # @param [Parser::Diagnostic] diagnostic
82
- # @return [TrueClass|FalseClass]
82
+ # @return [Boolean]
83
83
  #
84
84
  def ignore?(diagnostic)
85
85
  @ignore_warnings &&
@@ -90,7 +90,7 @@ module Parser
90
90
  # Checks whether `diagnostic` should be raised as an exception.
91
91
  #
92
92
  # @param [Parser::Diagnostic] diagnostic
93
- # @return [TrueClass|FalseClass]
93
+ # @return [Boolean]
94
94
  #
95
95
  def raise?(diagnostic)
96
96
  (@all_errors_are_fatal &&
@@ -259,7 +259,7 @@ class Parser::Lexer
259
259
  elsif @cs == self.class.lex_error
260
260
  [ false, [ '$error', range(p - 1, p) ] ]
261
261
  else
262
- [ false, [ '$eof', range(p - 1, p) ] ]
262
+ [ false, [ '$eof', range(p, p) ] ]
263
263
  end
264
264
  end
265
265
 
@@ -1,4 +1,5 @@
1
1
  module Parser
2
+
2
3
  ##
3
4
  # {Parser::Rewriter} offers a basic API that makes it easy to rewrite
4
5
  # existing ASTs. It's built on top of {Parser::AST::Processor} and
@@ -7,6 +8,8 @@ module Parser
7
8
  # For example, assume you want to remove `do` tokens from a while statement.
8
9
  # You can do this as following:
9
10
  #
11
+ # require 'parser/current'
12
+ #
10
13
  # class RemoveDo < Parser::Rewriter
11
14
  # def on_while(node)
12
15
  # # Check if the statement starts with "do"
@@ -40,6 +43,9 @@ module Parser
40
43
  # Keep in mind that {Parser::Rewriter} does not take care of indentation when
41
44
  # inserting/replacing code so you'll have to do this yourself.
42
45
  #
46
+ # See also [a blog entry](http://whitequark.org/blog/2013/04/26/lets-play-with-ruby-code/)
47
+ # describing rewriters in greater detail.
48
+ #
43
49
  # @api public
44
50
  #
45
51
  class Rewriter < Parser::AST::Processor
@@ -59,14 +65,12 @@ module Parser
59
65
  @source_rewriter.process
60
66
  end
61
67
 
62
- private
63
-
64
68
  ##
65
69
  # Returns `true` if the specified node is an assignment node, returns false
66
70
  # otherwise.
67
71
  #
68
72
  # @param [Parser::AST::Node] node
69
- # @return [TrueClass|FalseClass]
73
+ # @return [Boolean]
70
74
  #
71
75
  def assignment?(node)
72
76
  [:lvasgn, :ivasgn, :gvasgn, :cvasgn, :casgn].include?(node.type)
@@ -111,4 +115,5 @@ module Parser
111
115
  @source_rewriter.replace(range, content)
112
116
  end
113
117
  end
118
+
114
119
  end
@@ -1,14 +1,32 @@
1
- # encoding:ascii-8bit
1
+ # encoding: ascii-8bit
2
2
 
3
3
  module Parser
4
4
  module Source
5
5
 
6
6
  ##
7
+ # A buffer with source code. {Buffer} contains the source code itself,
8
+ # associated location information (name and first line), and takes care
9
+ # of encoding.
10
+ #
11
+ # A source buffer is immutable once populated.
12
+ #
13
+ # @!attribute [r] name
14
+ # Buffer name. If the buffer was created from a file, the name corresponds
15
+ # to relative path to the file.
16
+ # @return [String] buffer name
17
+ #
18
+ # @!attribute [r] first_line
19
+ # First line of the buffer, 1 by default.
20
+ # @return [Integer] first line
21
+ #
7
22
  # @api public
8
23
  #
9
24
  class Buffer
10
25
  attr_reader :name, :first_line
11
26
 
27
+ ##
28
+ # @api private
29
+ #
12
30
  ENCODING_RE =
13
31
  /\#.*coding\s*[:=]\s*
14
32
  (
@@ -22,6 +40,13 @@ module Parser
22
40
  )
23
41
  /x
24
42
 
43
+ ##
44
+ # Try to recognize encoding of `string` as Ruby would, i.e. by looking for
45
+ # magic encoding comment or UTF-8 BOM. `string` can be in any encoding.
46
+ #
47
+ # @param [String] string
48
+ # @return [String|nil] encoding name, if recognized
49
+ #
25
50
  def self.recognize_encoding(string)
26
51
  return if string.empty?
27
52
 
@@ -44,20 +69,31 @@ module Parser
44
69
  end
45
70
  end
46
71
 
47
- # Lexer expects UTF-8 input. This method processes the input
48
- # in an arbitrary valid Ruby encoding and returns an UTF-8 encoded
49
- # string.
72
+ ##
73
+ # Recognize encoding of `input` and process it so it could be lexed.
74
+ #
75
+ # * If `input` does not contain BOM or magic encoding comment, it is
76
+ # kept in the original encoding.
77
+ # * If the detected encoding is binary, `input` is kept in binary.
78
+ # * Otherwise, `input` is re-encoded into UTF-8 and returned as a
79
+ # new string.
80
+ #
81
+ # This method mutates the encoding of `input`, but not its content.
50
82
  #
51
- def self.reencode_string(string)
52
- original_encoding = string.encoding
53
- detected_encoding = recognize_encoding(string.force_encoding(Encoding::BINARY))
83
+ # @param [String] input
84
+ # @return [String]
85
+ # @raise [EncodingError]
86
+ #
87
+ def self.reencode_string(input)
88
+ original_encoding = input.encoding
89
+ detected_encoding = recognize_encoding(input.force_encoding(Encoding::BINARY))
54
90
 
55
91
  if detected_encoding.nil?
56
- string.force_encoding(original_encoding)
92
+ input.force_encoding(original_encoding)
57
93
  elsif detected_encoding == Encoding::BINARY
58
- string
94
+ input
59
95
  else
60
- string.
96
+ input.
61
97
  force_encoding(detected_encoding).
62
98
  encode(Encoding::UTF_8)
63
99
  end
@@ -72,6 +108,15 @@ module Parser
72
108
  @line_begins = nil
73
109
  end
74
110
 
111
+ ##
112
+ # Populate this buffer from correspondingly named file.
113
+ #
114
+ # @example
115
+ # Parser::Source::Buffer.new('foo/bar.rb').read
116
+ #
117
+ # @return [Buffer] self
118
+ # @raise [ArgumentError] if already populated
119
+ #
75
120
  def read
76
121
  File.open(@name, 'rb') do |io|
77
122
  self.source = io.read
@@ -80,6 +125,12 @@ module Parser
80
125
  self
81
126
  end
82
127
 
128
+ ##
129
+ # Source code contained in this buffer.
130
+ #
131
+ # @return [String] source code
132
+ # @raise [RuntimeError] if buffer is not populated yet
133
+ #
83
134
  def source
84
135
  if @source.nil?
85
136
  raise RuntimeError, 'Cannot extract source from uninitialized Source::Buffer'
@@ -88,41 +139,68 @@ module Parser
88
139
  @source
89
140
  end
90
141
 
91
- def source=(source)
142
+ ##
143
+ # Populate this buffer from a string with encoding autodetection.
144
+ # `input` is mutated if not frozen.
145
+ #
146
+ # @param [String] input
147
+ # @raise [ArgumentError] if already populated
148
+ # @return [String]
149
+ #
150
+ def source=(input)
92
151
  if defined?(Encoding)
93
- source = source.dup if source.frozen?
94
- source = self.class.reencode_string(source)
152
+ input = input.dup if input.frozen?
153
+ input = self.class.reencode_string(input)
95
154
  end
96
155
 
97
- self.raw_source = source
156
+ self.raw_source = input
98
157
  end
99
158
 
100
- def raw_source=(source)
159
+ ##
160
+ # Populate this buffer from a string without encoding autodetection.
161
+ #
162
+ # @param [String] input
163
+ # @raise [ArgumentError] if already populated
164
+ # @return [String]
165
+ #
166
+ def raw_source=(input)
101
167
  if @source
102
168
  raise ArgumentError, 'Source::Buffer is immutable'
103
169
  end
104
170
 
105
- @source = source.gsub(/\r\n/, "\n").freeze
171
+ @source = input.gsub(/\r\n/, "\n").freeze
106
172
  end
107
173
 
174
+ ##
175
+ # Convert a character index into the source to a `[line, column]` tuple.
176
+ #
177
+ # @param [Integer] position
178
+ # @return [[Integer, Integer]] `[line, column]`
179
+ #
108
180
  def decompose_position(position)
109
181
  line_no, line_begin = line_for(position)
110
182
 
111
183
  [ @first_line + line_no, position - line_begin ]
112
184
  end
113
185
 
186
+ ##
187
+ # Extract line `lineno` from source, taking `first_line` into account.
188
+ #
189
+ # @param [Integer] lineno
190
+ # @return [String]
191
+ # @raise [IndexError] if `lineno` is out of bounds
192
+ #
114
193
  def source_line(lineno)
115
194
  unless @lines
116
195
  @lines = @source.lines.to_a
117
196
  @lines.each { |line| line.gsub!(/\n$/, '') }
118
197
 
119
- # Lexer has an "infinite stream of EOF symbols" after the
120
- # actual EOF, so in some cases (e.g. EOF token of ruby-parse -E)
121
- # tokens will refer to one line past EOF.
198
+ # If a file ends with a newline, the EOF token will appear
199
+ # to be one line further than the end of file.
122
200
  @lines << ""
123
201
  end
124
202
 
125
- @lines[lineno - @first_line].dup
203
+ @lines.fetch(lineno - @first_line).dup
126
204
  end
127
205
 
128
206
  private
@@ -2,13 +2,16 @@ module Parser
2
2
  module Source
3
3
 
4
4
  ##
5
- # @api public
5
+ # A comment in the source code.
6
6
  #
7
7
  # @!attribute [r] text
8
- # @return String
8
+ # @return [String]
9
9
  #
10
10
  # @!attribute [r] location
11
- # @return Parser::Source::Map
11
+ # @return [Parser::Source::Map]
12
+ #
13
+ # @api public
14
+ #
12
15
  class Comment
13
16
  attr_reader :text
14
17
 
@@ -16,14 +19,22 @@ module Parser
16
19
  alias_method :loc, :location
17
20
 
18
21
  ##
22
+ # Associate `comments` with `ast` nodes by their location in the
23
+ # source.
24
+ #
25
+ # @param [Parser::AST::Node] ast
26
+ # @param [Array(Comment)] comments
27
+ # @return [Hash(Parser::AST::Node, Array(Comment))]
19
28
  # @see Parser::Source::Comment::Associator
29
+ #
20
30
  def self.associate(ast, comments)
21
- associator = Associator.new(comments, ast)
31
+ associator = Associator.new(ast, comments)
22
32
  associator.associate
23
33
  end
24
34
 
25
35
  ##
26
36
  # @param [Parser::Source::Range] range
37
+ #
27
38
  def initialize(range)
28
39
  @location = Parser::Source::Map.new(range)
29
40
  @text = range.source.freeze
@@ -32,7 +43,7 @@ module Parser
32
43
  end
33
44
 
34
45
  ##
35
- # Returns the type of this comment.
46
+ # Type of this comment.
36
47
  #
37
48
  # * Inline comments correspond to `:inline`:
38
49
  #
@@ -43,6 +54,9 @@ module Parser
43
54
  # =begin
44
55
  # hi i am a document
45
56
  # =end
57
+ #
58
+ # @return [Symbol]
59
+ #
46
60
  def type
47
61
  case text
48
62
  when /^#/
@@ -53,28 +67,39 @@ module Parser
53
67
  end
54
68
 
55
69
  ##
56
- # @see [#type]
57
- # @return [TrueClass|FalseClass]
70
+ # @see #type
71
+ # @return [Boolean] true if this is an inline comment.
72
+ #
58
73
  def inline?
59
74
  type == :inline
60
75
  end
61
76
 
62
77
  ##
63
- # @see [#type]
64
- # @return [TrueClass|FalseClass]
78
+ # @see #type
79
+ # @return [Boolean] true if this is a block comment.
80
+ #
65
81
  def document?
66
82
  type == :document
67
83
  end
68
84
 
69
85
  ##
70
- # Compares comments. Two comments are identical if they
86
+ # Compares comments. Two comments are equal if they
71
87
  # correspond to the same source range.
88
+ #
72
89
  # @param [Object] other
73
- # @return [TrueClass|FalseClass]
90
+ # @return [Boolean]
91
+ #
74
92
  def ==(other)
75
93
  other.is_a?(Source::Comment) &&
76
94
  @location == other.location
77
95
  end
96
+
97
+ ##
98
+ # @return [String] a human-readable representation of this comment
99
+ #
100
+ def inspect
101
+ "#<Parser::Source::Comment #{@location.expression.to_s} #{text.inspect}>"
102
+ end
78
103
  end
79
104
 
80
105
  end
@@ -2,26 +2,82 @@ module Parser
2
2
  module Source
3
3
 
4
4
  ##
5
+ # A processor which associates AST nodes with comments based on their
6
+ # location in source code. It may be used, for example, to implement
7
+ # rdoc-style processing.
8
+ #
9
+ # @example
10
+ # require 'parser/current'
11
+ #
12
+ # ast, comments = Parser::CurrentRuby.parse_with_comments(<<-CODE)
13
+ # # Class stuff
14
+ # class Foo
15
+ # # Attr stuff
16
+ # # @see bar
17
+ # attr_accessor :foo
18
+ # end
19
+ # CODE
20
+ #
21
+ # p Parser::Source::Comment.associate(ast, comments)
22
+ # # => {
23
+ # # (class (const nil :Foo) ...) =>
24
+ # # [#<Parser::Source::Comment (string):1:1 "# Class stuff">],
25
+ # # (send nil :attr_accessor (sym :foo)) =>
26
+ # # [#<Parser::Source::Comment (string):3:3 "# Attr stuff">,
27
+ # # #<Parser::Source::Comment (string):4:3 "# @see bar">]
28
+ # # }
29
+ #
30
+ # @see #associate
5
31
  #
6
32
  # @!attribute skip_directives
7
- # Skip file processing directives disguised as comments,
8
- # namely:
33
+ # Skip file processing directives disguised as comments.
34
+ # Namely:
9
35
  #
10
36
  # * Shebang line,
11
37
  # * Magic encoding comment.
12
38
  #
39
+ # @return [Boolean]
40
+ #
13
41
  # @api public
14
42
  #
15
43
  class Comment::Associator
16
44
  attr_accessor :skip_directives
17
45
 
18
- def initialize(comments, ast)
19
- @comments = comments
46
+ ##
47
+ # @param [Parser::AST::Node] ast
48
+ # @param [Array(Parser::Source::Comment)] comments
49
+ def initialize(ast, comments)
20
50
  @ast = ast
51
+ @comments = comments
21
52
 
22
53
  @skip_directives = true
23
54
  end
24
55
 
56
+ ##
57
+ # Compute a mapping between AST nodes and comments.
58
+ #
59
+ # A comment belongs to a certain node if it begins after end
60
+ # of the previous node (if one exists) and ends before beginning of
61
+ # the current node.
62
+ #
63
+ # This rule is unambiguous and produces the result
64
+ # one could reasonably expect; for example, this code
65
+ #
66
+ # # foo
67
+ # hoge # bar
68
+ # + fuga
69
+ #
70
+ # will result in the following association:
71
+ #
72
+ # {
73
+ # (send (lvar :hoge) :+ (lvar :fuga)) =>
74
+ # [#<Parser::Source::Comment (string):2:1 "# foo">],
75
+ # (lvar :fuga) =>
76
+ # [#<Parser::Source::Comment (string):3:8 "# bar">]
77
+ # }
78
+ #
79
+ # @return [Hash(Parser::AST::Node, Array(Parser::Source::Comment))]
80
+ #
25
81
  def associate
26
82
  @mapping = Hash.new { |h, k| h[k] = [] }
27
83
  @comment_num = 0
@@ -2,29 +2,99 @@ module Parser
2
2
  module Source
3
3
 
4
4
  ##
5
+ # {Map} relates AST nodes to the source code they were parsed from.
6
+ # More specifically, a {Map} or its subclass contains a set of ranges:
7
+ #
8
+ # * `expression`: smallest range which includes all source corresponding
9
+ # to the node and all `expression` ranges of its children.
10
+ # * other ranges (`begin`, `end`, `operator`, ...): node-specific ranges
11
+ # pointing to various interesting tokens corresponding to the node.
12
+ #
13
+ # All ranges except `expression` are defined by {Map} subclasses.
14
+ #
15
+ # Ranges (except `expression`) can be `nil` if the corresponding token is
16
+ # not present in source. For example, a hash may not have opening/closing
17
+ # braces, and so would its source map.
18
+ #
19
+ # p Parser::CurrentRuby.parse('[1 => 2]').children[0].loc
20
+ # # => <Parser::Source::Map::Collection:0x007f5492b547d8
21
+ # # @end=nil, @begin=nil,
22
+ # # @expression=#<Source::Range (string) 1...7>>
23
+ #
24
+ # The {file:doc/AST_FORMAT.md} document describes how ranges associated to source
25
+ # code tokens. For example, the entry
26
+ #
27
+ # (array (int 1) (int 2))
28
+ #
29
+ # "[1, 2]"
30
+ # ^ begin
31
+ # ^ end
32
+ # ~~~~~~ expression
33
+ #
34
+ # means that if `node` is an {Parser::AST::Node} `(array (int 1) (int 2))`,
35
+ # then `node.loc` responds to `begin`, `end` and `expression`, and
36
+ # `node.loc.begin` returns a range pointing at the opening bracket, and so on.
37
+ #
38
+ # If you want to write code polymorphic by the source map (i.e. accepting
39
+ # several subclasses of {Map}), use `respond_to?` instead of `is_a?` to
40
+ # check whether the map features the range you need. Concrete {Map}
41
+ # subclasses may not be preserved between versions, but their interfaces
42
+ # will be kept compatible.
43
+ #
44
+ # You can visualize the source maps with `ruby-parse -E` command-line tool.
45
+ #
46
+ # @example
47
+ # require 'parser/current'
48
+ #
49
+ # p Parser::CurrentRuby.parse('[1, 2]').loc
50
+ # # => #<Parser::Source::Map::Collection:0x007f14b80eccd8
51
+ # # @end=#<Source::Range (string) 5...6>,
52
+ # # @begin=#<Source::Range (string) 0...1>,
53
+ # # @expression=#<Source::Range (string) 0...6>>
54
+ #
55
+ # @!attribute [r] expression
56
+ # @return [Range]
57
+ #
5
58
  # @api public
6
59
  #
7
60
  class Map
8
61
  attr_reader :expression
9
62
 
63
+ ##
64
+ # @param [Range] expression
10
65
  def initialize(expression)
11
66
  @expression = expression
12
67
 
13
68
  freeze
14
69
  end
15
70
 
71
+ ##
72
+ # A shortcut for `self.expression.line`.
73
+ # @return [Integer]
74
+ #
16
75
  def line
17
76
  @expression.line
18
77
  end
19
78
 
79
+ ##
80
+ # A shortcut for `self.expression.column`.
81
+ # @return [Integer]
82
+ #
20
83
  def column
21
84
  @expression.column
22
85
  end
23
86
 
87
+ ##
88
+ # @api private
89
+ #
24
90
  def with_expression(expression_l)
25
91
  with { |map| map.update_expression(expression_l) }
26
92
  end
27
93
 
94
+ ##
95
+ # Compares source maps.
96
+ # @return [Boolean]
97
+ #
28
98
  def ==(other)
29
99
  other.class == self.class &&
30
100
  instance_variables.map do |ivar|
@@ -33,6 +103,24 @@ module Parser
33
103
  end.reduce(:&)
34
104
  end
35
105
 
106
+ ##
107
+ # Converts this source map to a hash with keys corresponding to
108
+ # ranges. For example, if called on an instance of {Collection},
109
+ # which adds the `begin` and `end` ranges, the resulting hash
110
+ # will contain keys `:expression`, `:begin` and `:end`.
111
+ #
112
+ # @example
113
+ # require 'parser/current'
114
+ #
115
+ # p Parser::CurrentRuby.parse('[1, 2]').loc.to_hash
116
+ # # => {
117
+ # # :begin => #<Source::Range (string) 0...1>,
118
+ # # :end => #<Source::Range (string) 5...6>,
119
+ # # :expression => #<Source::Range (string) 0...6>
120
+ # # }
121
+ #
122
+ # @return [Hash(Symbol, Parser::Source::Range)]
123
+ #
36
124
  def to_hash
37
125
  Hash[instance_variables.map do |ivar|
38
126
  [ ivar[1..-1].to_sym, instance_variable_get(ivar) ]
@@ -12,6 +12,9 @@ module Parser
12
12
  super(expression)
13
13
  end
14
14
 
15
+ ##
16
+ # @api private
17
+ #
15
18
  def with_operator(operator_l)
16
19
  with { |map| map.update_operator(operator_l) }
17
20
  end
@@ -16,6 +16,9 @@ module Parser
16
16
  super(expression_l)
17
17
  end
18
18
 
19
+ ##
20
+ # @api private
21
+ #
19
22
  def with_operator(operator_l)
20
23
  with { |map| map.update_operator(operator_l) }
21
24
  end
@@ -11,6 +11,9 @@ module Parser
11
11
  super(expression_l)
12
12
  end
13
13
 
14
+ ##
15
+ # @api private
16
+ #
14
17
  def with_operator(operator_l)
15
18
  with { |map| map.update_operator(operator_l) }
16
19
  end
@@ -2,12 +2,34 @@ module Parser
2
2
  module Source
3
3
 
4
4
  ##
5
+ # A range of characters in a particular source buffer.
6
+ #
7
+ # The range is always exclusive, i.e. a range with `begin_pos` of 3 and
8
+ # `end_pos` of 5 will contain the following characters:
9
+ #
10
+ # example
11
+ # ^^
12
+ #
13
+ # @!attribute [r] source_buffer
14
+ # @return [Parser::Diagnostic::Engine]
15
+ #
16
+ # @!attribute [r] begin_pos
17
+ # @return [Integer] index of the first character in the range
18
+ #
19
+ # @!attribute [r] end_pos
20
+ # @return [Integer] index of the character after the last character in the range
21
+ #
5
22
  # @api public
6
23
  #
7
24
  class Range
8
25
  attr_reader :source_buffer
9
26
  attr_reader :begin_pos, :end_pos
10
27
 
28
+ ##
29
+ # @param [Buffer] source_buffer
30
+ # @param [Integer] begin_pos
31
+ # @param [Integer] end_pos
32
+ #
11
33
  def initialize(source_buffer, begin_pos, end_pos)
12
34
  @source_buffer = source_buffer
13
35
  @begin_pos, @end_pos = begin_pos, end_pos
@@ -15,68 +37,137 @@ module Parser
15
37
  freeze
16
38
  end
17
39
 
40
+ ##
41
+ # @return [Range] a zero-length range located just before the beginning
42
+ # of this range.
43
+ #
18
44
  def begin
19
45
  Range.new(@source_buffer, @begin_pos, @begin_pos)
20
46
  end
21
47
 
48
+ ##
49
+ # @return [Range] a zero-length range located just after the end
50
+ # of this range.
51
+ #
22
52
  def end
23
53
  Range.new(@source_buffer, @end_pos, @end_pos)
24
54
  end
25
55
 
56
+ ##
57
+ # @return [Integer] amount of characters included in this range.
58
+ #
26
59
  def size
27
60
  @end_pos - @begin_pos
28
61
  end
29
62
 
30
63
  alias length size
31
64
 
65
+ ##
66
+ # Line number of the beginning of this range. By default, the first line
67
+ # of a buffer is 1; as such, line numbers are most commonly one-based.
68
+ #
69
+ # @see Buffer
70
+ # @return [Integer] line number of the beginning of this range.
71
+ #
32
72
  def line
33
73
  line, _ = @source_buffer.decompose_position(@begin_pos)
34
74
 
35
75
  line
36
76
  end
37
77
 
78
+ ##
79
+ # @return [Integer] zero-based column number of the beginning of this range.
80
+ #
38
81
  def column
39
82
  _, column = @source_buffer.decompose_position(@begin_pos)
40
83
 
41
84
  column
42
85
  end
43
86
 
87
+ ##
88
+ # @return [::Range] a range of columns spanned by this range.
89
+ # @raise RangeError
90
+ #
44
91
  def column_range
92
+ if self.begin.line != self.end.line
93
+ raise RangeError, "#{self.inspect} spans more than one line"
94
+ end
95
+
45
96
  self.begin.column...self.end.column
46
97
  end
47
98
 
99
+ ##
100
+ # @return [String] a line of source code containing the beginning of this range.
101
+ #
48
102
  def source_line
49
103
  @source_buffer.source_line(line)
50
104
  end
51
105
 
106
+ ##
107
+ # @return [String] all source code covered by this range.
108
+ #
52
109
  def source
53
110
  @source_buffer.source[self.begin_pos...self.end_pos]
54
111
  end
55
112
 
113
+ ##
114
+ # `is?` provides a concise way to compare the source corresponding to this range.
115
+ # For example, `r.source == '(' || r.source == 'begin'` is equivalent to
116
+ # `r.is?('(', 'begin')`.
117
+ #
56
118
  def is?(*what)
57
119
  what.include?(source)
58
120
  end
59
121
 
122
+ ##
123
+ # @return [Array(Integer)] a set of character indexes contained in this range.
124
+ #
60
125
  def to_a
61
126
  (@begin_pos...@end_pos).to_a
62
127
  end
63
128
 
129
+ ##
130
+ # Composes a GNU/Clang-style string representation of the beginning of this
131
+ # range.
132
+ #
133
+ # For example, for the following range in file `foo.rb`,
134
+ #
135
+ # def foo
136
+ # ^^^
137
+ #
138
+ # `to_s` will return `foo.rb:1:5`.
139
+ # Note that the column index is one-based.
140
+ #
141
+ # @return [String]
142
+ #
64
143
  def to_s
65
144
  line, column = @source_buffer.decompose_position(@begin_pos)
66
145
 
67
146
  [@source_buffer.name, line, column + 1].join(':')
68
147
  end
69
148
 
149
+ ##
150
+ # @param [Integer] new_size
151
+ # @return [Range] a range beginning at the same point as this range and length `new_size`.
152
+ #
70
153
  def resize(new_size)
71
154
  Range.new(@source_buffer, @begin_pos, @begin_pos + new_size)
72
155
  end
73
156
 
157
+ ##
158
+ # @param [Range] other
159
+ # @return [Range] smallest possible range spanning both this range and `other`.
160
+ #
74
161
  def join(other)
75
162
  Range.new(@source_buffer,
76
163
  [@begin_pos, other.begin_pos].min,
77
164
  [@end_pos, other.end_pos].max)
78
165
  end
79
166
 
167
+ ##
168
+ # Compares ranges.
169
+ # @return [Boolean]
170
+ #
80
171
  def ==(other)
81
172
  other.is_a?(Range) &&
82
173
  @source_buffer == other.source_buffer &&
@@ -84,8 +175,11 @@ module Parser
84
175
  @end_pos == other.end_pos
85
176
  end
86
177
 
178
+ ##
179
+ # @return [String] a human-readable representation of this range.
180
+ #
87
181
  def inspect
88
- "#<Source::Range #{@source_buffer.name} #{@begin_pos}...#{@end_pos}>"
182
+ "#<Parser::Source::Range #{@source_buffer.name} #{@begin_pos}...#{@end_pos}>"
89
183
  end
90
184
  end
91
185
 
@@ -2,11 +2,32 @@ module Parser
2
2
  module Source
3
3
 
4
4
  ##
5
+ # {Rewriter} performs the heavy lifting in the source rewriting process.
6
+ # It schedules code updates to be performed in the correct order and
7
+ # verifies that no two updates _clobber_ each other, that is, attempt to
8
+ # modify the same part of code.
9
+ #
10
+ # If it is detected that one update clobbers another one, an `:error` and
11
+ # a `:note` diagnostics describing both updates are generated and passed to
12
+ # the diagnostic engine. After that, an exception is raised.
13
+ #
14
+ # The default diagnostic engine consumer simply prints the diagnostics to `stderr`.
15
+ #
16
+ # @!attribute [r] source_buffer
17
+ # @return [Source::Buffer]
18
+ #
19
+ # @!attribute [r] diagnostics
20
+ # @return [Diagnostic::Engine]
21
+ #
5
22
  # @api public
6
23
  #
7
24
  class Rewriter
8
- attr_accessor :diagnostics
25
+ attr_reader :source_buffer
26
+ attr_reader :diagnostics
9
27
 
28
+ ##
29
+ # @param [Source::Buffer] source_buffer
30
+ #
10
31
  def initialize(source_buffer)
11
32
  @diagnostics = Diagnostic::Engine.new
12
33
  @diagnostics.consumer = lambda do |diag|
@@ -18,28 +39,67 @@ module Parser
18
39
  @clobber = 0
19
40
  end
20
41
 
42
+ ##
43
+ # Removes the source range.
44
+ #
45
+ # @param [Range] range
46
+ # @return [Rewriter] self
47
+ # @raise [RuntimeError] when clobbering is detected
48
+ #
21
49
  def remove(range)
22
50
  append Rewriter::Action.new(range, '')
23
51
  end
24
52
 
53
+ ##
54
+ # Inserts new code before the given source range.
55
+ #
56
+ # @param [Range] range
57
+ # @param [String] content
58
+ # @return [Rewriter] self
59
+ # @raise [RuntimeError] when clobbering is detected
60
+ #
25
61
  def insert_before(range, content)
26
62
  append Rewriter::Action.new(range.begin, content)
27
63
  end
28
64
 
65
+ ##
66
+ # Inserts new code after the given source range.
67
+ #
68
+ # @param [Range] range
69
+ # @param [String] content
70
+ # @return [Rewriter] self
71
+ # @raise [RuntimeError] when clobbering is detected
72
+ #
29
73
  def insert_after(range, content)
30
74
  append Rewriter::Action.new(range.end, content)
31
75
  end
32
76
 
77
+ ##
78
+ # Replaces the code of the source range `range` with `content`.
79
+ #
80
+ # @param [Range] range
81
+ # @param [String] content
82
+ # @return [Rewriter] self
83
+ # @raise [RuntimeError] when clobbering is detected
84
+ #
33
85
  def replace(range, content)
34
86
  append Rewriter::Action.new(range, content)
35
87
  end
36
88
 
89
+ ##
90
+ # Applies all scheduled changes to the `source_buffer` and returns
91
+ # modified source as a new string.
92
+ #
93
+ # @return [String]
94
+ #
37
95
  def process
38
- adjustment = 0
39
- source = @source_buffer.source.dup
96
+ adjustment = 0
97
+ source = @source_buffer.source.dup
98
+
40
99
  sorted_queue = @queue.sort_by.with_index do |action, index|
41
100
  [action.range.begin_pos, index]
42
101
  end
102
+
43
103
  sorted_queue.each do |action|
44
104
  begin_pos = action.range.begin_pos + adjustment
45
105
  end_pos = begin_pos + action.range.length
@@ -67,6 +127,8 @@ module Parser
67
127
  "clobbered by: #{clobber_action}",
68
128
  clobber_action.range)
69
129
  @diagnostics.process(diagnostic)
130
+
131
+ raise RuntimeError, "Parser::Source::Rewriter detected clobbering"
70
132
  else
71
133
  clobber(action.range)
72
134
 
@@ -1,3 +1,3 @@
1
1
  module Parser
2
- VERSION = '2.0.0.pre8'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -35,7 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_development_dependency 'simplecov', '~> 0.7'
36
36
  spec.add_development_dependency 'coveralls'
37
37
  spec.add_development_dependency 'json_pure' # for coveralls on 1.9.2
38
- spec.add_development_dependency 'cliver' # executable version detection
38
+ spec.add_development_dependency 'cliver', '~> 0.2.2' # executable version detection
39
39
 
40
40
  spec.add_development_dependency 'simplecov-sublime-ruby-coverage'
41
41
 
@@ -74,9 +74,11 @@ class TestSourceRewriter < Minitest::Test
74
74
  diagnostics << diag
75
75
  end
76
76
 
77
- @rewriter.
78
- replace(range(3, 1), '---').
79
- remove(range(3, 1))
77
+ assert_raises RuntimeError, /clobber/ do
78
+ @rewriter.
79
+ replace(range(3, 1), '---').
80
+ remove(range(3, 1))
81
+ end
80
82
 
81
83
  assert_equal 2, diagnostics.count
82
84
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre8
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Zotov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-15 00:00:00.000000000 Z
11
+ date: 2013-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast
@@ -146,16 +146,16 @@ dependencies:
146
146
  name: cliver
147
147
  requirement: !ruby/object:Gem::Requirement
148
148
  requirements:
149
- - - '>='
149
+ - - ~>
150
150
  - !ruby/object:Gem::Version
151
- version: '0'
151
+ version: 0.2.2
152
152
  type: :development
153
153
  prerelease: false
154
154
  version_requirements: !ruby/object:Gem::Requirement
155
155
  requirements:
156
- - - '>='
156
+ - - ~>
157
157
  - !ruby/object:Gem::Version
158
- version: '0'
158
+ version: 0.2.2
159
159
  - !ruby/object:Gem::Dependency
160
160
  name: simplecov-sublime-ruby-coverage
161
161
  requirement: !ruby/object:Gem::Requirement
@@ -246,6 +246,7 @@ files:
246
246
  - lib/parser/base.rb
247
247
  - lib/parser/builders/default.rb
248
248
  - lib/parser/compatibility/ruby1_8.rb
249
+ - lib/parser/compatibility/ruby1_9.rb
249
250
  - lib/parser/current.rb
250
251
  - lib/parser/diagnostic.rb
251
252
  - lib/parser/diagnostic/engine.rb
@@ -321,9 +322,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
321
322
  version: '0'
322
323
  required_rubygems_version: !ruby/object:Gem::Requirement
323
324
  requirements:
324
- - - '>'
325
+ - - '>='
325
326
  - !ruby/object:Gem::Version
326
- version: 1.3.1
327
+ version: '0'
327
328
  requirements: []
328
329
  rubyforge_project:
329
330
  rubygems_version: 2.0.0