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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +8 -7
- data/doc/AST_FORMAT.md +2 -0
- data/lib/parser.rb +4 -0
- data/lib/parser/base.rb +2 -1
- data/lib/parser/builders/default.rb +1 -1
- data/lib/parser/compatibility/ruby1_9.rb +32 -0
- data/lib/parser/diagnostic/engine.rb +4 -4
- data/lib/parser/lexer.rl +1 -1
- data/lib/parser/rewriter.rb +8 -3
- data/lib/parser/source/buffer.rb +98 -20
- data/lib/parser/source/comment.rb +36 -11
- data/lib/parser/source/comment/associator.rb +60 -4
- data/lib/parser/source/map.rb +88 -0
- data/lib/parser/source/map/constant.rb +3 -0
- data/lib/parser/source/map/send.rb +3 -0
- data/lib/parser/source/map/variable.rb +3 -0
- data/lib/parser/source/range.rb +95 -1
- data/lib/parser/source/rewriter.rb +65 -3
- data/lib/parser/version.rb +1 -1
- data/parser.gemspec +1 -1
- data/test/test_source_rewriter.rb +5 -3
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf0b6a5c97a393fd82aae713e9a10d4aa2108a0f
|
4
|
+
data.tar.gz: cf126c874ffc8d2430ccec47583b9dfca2afc7a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 07a91a9884e3f0a22010c7cf60089de7b4bd4e0285df85771b9f0b52317d3b6930ba6bea44fa57c4c168723a92a3ccf4602b354a9e9e55ed7461e1009fa8431b
|
7
|
+
data.tar.gz: c95ab5caef72d3c0f936def4dd0dcb28298fbc335c9558d9d642edcfb9398a7f2b6479983cd5406ca62512f07c864b1884f4d0269bb34fb7fe75182a5086984c
|
data/CHANGELOG.md
CHANGED
@@ -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:
|
35
|
-
# @
|
36
|
-
# @
|
37
|
-
# @
|
38
|
-
# @selector=#<Source::Range (string) 2...3
|
39
|
-
|
40
|
-
|
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:
|
data/doc/AST_FORMAT.md
CHANGED
data/lib/parser.rb
CHANGED
data/lib/parser/base.rb
CHANGED
@@ -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 [
|
221
|
+
# @return [Boolean]
|
221
222
|
#
|
222
223
|
def in_def?
|
223
224
|
@def_level > 0
|
@@ -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 [
|
28
|
+
# @return [Boolean]
|
29
29
|
#
|
30
30
|
# @!attribute [rw] ignore_warnings
|
31
31
|
# When set to `true` warnings will be ignored.
|
32
|
-
# @return [
|
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 [
|
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 [
|
93
|
+
# @return [Boolean]
|
94
94
|
#
|
95
95
|
def raise?(diagnostic)
|
96
96
|
(@all_errors_are_fatal &&
|
data/lib/parser/lexer.rl
CHANGED
data/lib/parser/rewriter.rb
CHANGED
@@ -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 [
|
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
|
data/lib/parser/source/buffer.rb
CHANGED
@@ -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
|
-
|
48
|
-
#
|
49
|
-
#
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
92
|
+
input.force_encoding(original_encoding)
|
57
93
|
elsif detected_encoding == Encoding::BINARY
|
58
|
-
|
94
|
+
input
|
59
95
|
else
|
60
|
-
|
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
|
-
|
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
|
-
|
94
|
-
|
152
|
+
input = input.dup if input.frozen?
|
153
|
+
input = self.class.reencode_string(input)
|
95
154
|
end
|
96
155
|
|
97
|
-
self.raw_source =
|
156
|
+
self.raw_source = input
|
98
157
|
end
|
99
158
|
|
100
|
-
|
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 =
|
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
|
-
#
|
120
|
-
#
|
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
|
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
|
-
#
|
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(
|
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
|
-
#
|
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
|
57
|
-
# @return [
|
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
|
64
|
-
# @return [
|
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
|
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 [
|
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
|
-
#
|
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
|
-
|
19
|
-
|
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
|
data/lib/parser/source/map.rb
CHANGED
@@ -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) ]
|
data/lib/parser/source/range.rb
CHANGED
@@ -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
|
-
|
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
|
39
|
-
source
|
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
|
|
data/lib/parser/version.rb
CHANGED
data/parser.gemspec
CHANGED
@@ -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
|
-
|
78
|
-
|
79
|
-
|
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
|
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-
|
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:
|
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:
|
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:
|
327
|
+
version: '0'
|
327
328
|
requirements: []
|
328
329
|
rubyforge_project:
|
329
330
|
rubygems_version: 2.0.0
|