parser 2.0.0.pre8 → 2.0.0

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.
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