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