jruby-prism-parser 0.23.0.pre.SNAPSHOT-java

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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +401 -0
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/CONTRIBUTING.md +62 -0
  5. data/LICENSE.md +7 -0
  6. data/Makefile +101 -0
  7. data/README.md +98 -0
  8. data/config.yml +2902 -0
  9. data/docs/build_system.md +91 -0
  10. data/docs/configuration.md +64 -0
  11. data/docs/cruby_compilation.md +27 -0
  12. data/docs/design.md +53 -0
  13. data/docs/encoding.md +121 -0
  14. data/docs/fuzzing.md +88 -0
  15. data/docs/heredocs.md +36 -0
  16. data/docs/javascript.md +118 -0
  17. data/docs/local_variable_depth.md +229 -0
  18. data/docs/mapping.md +117 -0
  19. data/docs/parser_translation.md +34 -0
  20. data/docs/parsing_rules.md +19 -0
  21. data/docs/releasing.md +98 -0
  22. data/docs/ripper.md +36 -0
  23. data/docs/ruby_api.md +43 -0
  24. data/docs/ruby_parser_translation.md +19 -0
  25. data/docs/serialization.md +209 -0
  26. data/docs/testing.md +55 -0
  27. data/ext/prism/api_node.c +5098 -0
  28. data/ext/prism/api_pack.c +267 -0
  29. data/ext/prism/extconf.rb +110 -0
  30. data/ext/prism/extension.c +1155 -0
  31. data/ext/prism/extension.h +18 -0
  32. data/include/prism/ast.h +5807 -0
  33. data/include/prism/defines.h +102 -0
  34. data/include/prism/diagnostic.h +339 -0
  35. data/include/prism/encoding.h +265 -0
  36. data/include/prism/node.h +57 -0
  37. data/include/prism/options.h +230 -0
  38. data/include/prism/pack.h +152 -0
  39. data/include/prism/parser.h +732 -0
  40. data/include/prism/prettyprint.h +26 -0
  41. data/include/prism/regexp.h +33 -0
  42. data/include/prism/util/pm_buffer.h +155 -0
  43. data/include/prism/util/pm_char.h +205 -0
  44. data/include/prism/util/pm_constant_pool.h +209 -0
  45. data/include/prism/util/pm_list.h +97 -0
  46. data/include/prism/util/pm_memchr.h +29 -0
  47. data/include/prism/util/pm_newline_list.h +93 -0
  48. data/include/prism/util/pm_state_stack.h +42 -0
  49. data/include/prism/util/pm_string.h +150 -0
  50. data/include/prism/util/pm_string_list.h +44 -0
  51. data/include/prism/util/pm_strncasecmp.h +32 -0
  52. data/include/prism/util/pm_strpbrk.h +46 -0
  53. data/include/prism/version.h +29 -0
  54. data/include/prism.h +289 -0
  55. data/jruby-prism.jar +0 -0
  56. data/lib/prism/compiler.rb +486 -0
  57. data/lib/prism/debug.rb +206 -0
  58. data/lib/prism/desugar_compiler.rb +207 -0
  59. data/lib/prism/dispatcher.rb +2150 -0
  60. data/lib/prism/dot_visitor.rb +4634 -0
  61. data/lib/prism/dsl.rb +785 -0
  62. data/lib/prism/ffi.rb +346 -0
  63. data/lib/prism/lex_compat.rb +908 -0
  64. data/lib/prism/mutation_compiler.rb +753 -0
  65. data/lib/prism/node.rb +17864 -0
  66. data/lib/prism/node_ext.rb +212 -0
  67. data/lib/prism/node_inspector.rb +68 -0
  68. data/lib/prism/pack.rb +224 -0
  69. data/lib/prism/parse_result/comments.rb +177 -0
  70. data/lib/prism/parse_result/newlines.rb +64 -0
  71. data/lib/prism/parse_result.rb +498 -0
  72. data/lib/prism/pattern.rb +250 -0
  73. data/lib/prism/serialize.rb +1354 -0
  74. data/lib/prism/translation/parser/compiler.rb +1838 -0
  75. data/lib/prism/translation/parser/lexer.rb +335 -0
  76. data/lib/prism/translation/parser/rubocop.rb +37 -0
  77. data/lib/prism/translation/parser.rb +178 -0
  78. data/lib/prism/translation/ripper.rb +577 -0
  79. data/lib/prism/translation/ruby_parser.rb +1521 -0
  80. data/lib/prism/translation.rb +11 -0
  81. data/lib/prism/version.rb +3 -0
  82. data/lib/prism/visitor.rb +495 -0
  83. data/lib/prism.rb +99 -0
  84. data/prism.gemspec +135 -0
  85. data/rbi/prism.rbi +7767 -0
  86. data/rbi/prism_static.rbi +207 -0
  87. data/sig/prism.rbs +4773 -0
  88. data/sig/prism_static.rbs +201 -0
  89. data/src/diagnostic.c +400 -0
  90. data/src/encoding.c +5132 -0
  91. data/src/node.c +2786 -0
  92. data/src/options.c +213 -0
  93. data/src/pack.c +493 -0
  94. data/src/prettyprint.c +8881 -0
  95. data/src/prism.c +18406 -0
  96. data/src/regexp.c +638 -0
  97. data/src/serialize.c +1554 -0
  98. data/src/token_type.c +700 -0
  99. data/src/util/pm_buffer.c +190 -0
  100. data/src/util/pm_char.c +318 -0
  101. data/src/util/pm_constant_pool.c +322 -0
  102. data/src/util/pm_list.c +49 -0
  103. data/src/util/pm_memchr.c +35 -0
  104. data/src/util/pm_newline_list.c +84 -0
  105. data/src/util/pm_state_stack.c +25 -0
  106. data/src/util/pm_string.c +203 -0
  107. data/src/util/pm_string_list.c +28 -0
  108. data/src/util/pm_strncasecmp.c +24 -0
  109. data/src/util/pm_strpbrk.c +180 -0
  110. metadata +156 -0
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Here we are reopening the prism module to provide methods on nodes that aren't
4
+ # templated and are meant as convenience methods.
5
+ module Prism
6
+ module RegularExpressionOptions # :nodoc:
7
+ # Returns a numeric value that represents the flags that were used to create
8
+ # the regular expression.
9
+ def options
10
+ o = flags & (RegularExpressionFlags::IGNORE_CASE | RegularExpressionFlags::EXTENDED | RegularExpressionFlags::MULTI_LINE)
11
+ o |= Regexp::FIXEDENCODING if flags.anybits?(RegularExpressionFlags::EUC_JP | RegularExpressionFlags::WINDOWS_31J | RegularExpressionFlags::UTF_8)
12
+ o |= Regexp::NOENCODING if flags.anybits?(RegularExpressionFlags::ASCII_8BIT)
13
+ o
14
+ end
15
+ end
16
+
17
+ class InterpolatedMatchLastLineNode < Node
18
+ include RegularExpressionOptions
19
+ end
20
+
21
+ class InterpolatedRegularExpressionNode < Node
22
+ include RegularExpressionOptions
23
+ end
24
+
25
+ class MatchLastLineNode < Node
26
+ include RegularExpressionOptions
27
+ end
28
+
29
+ class RegularExpressionNode < Node
30
+ include RegularExpressionOptions
31
+ end
32
+
33
+ private_constant :RegularExpressionOptions
34
+
35
+ module HeredocQuery # :nodoc:
36
+ # Returns true if this node was represented as a heredoc in the source code.
37
+ def heredoc?
38
+ opening&.start_with?("<<")
39
+ end
40
+ end
41
+
42
+ class InterpolatedStringNode < Node
43
+ include HeredocQuery
44
+ end
45
+
46
+ class InterpolatedXStringNode < Node
47
+ include HeredocQuery
48
+ end
49
+
50
+ class StringNode < Node
51
+ include HeredocQuery
52
+ end
53
+
54
+ class XStringNode < Node
55
+ include HeredocQuery
56
+ end
57
+
58
+ private_constant :HeredocQuery
59
+
60
+ class FloatNode < Node
61
+ # Returns the value of the node as a Ruby Float.
62
+ def value
63
+ Float(slice)
64
+ end
65
+ end
66
+
67
+ class ImaginaryNode < Node
68
+ # Returns the value of the node as a Ruby Complex.
69
+ def value
70
+ Complex(0, numeric.value)
71
+ end
72
+ end
73
+
74
+ class IntegerNode < Node
75
+ # Returns the value of the node as a Ruby Integer.
76
+ def value
77
+ Integer(slice)
78
+ end
79
+ end
80
+
81
+ class RationalNode < Node
82
+ # Returns the value of the node as a Ruby Rational.
83
+ def value
84
+ Rational(numeric.is_a?(IntegerNode) ? numeric.value : slice.chomp("r"))
85
+ end
86
+ end
87
+
88
+ class ConstantReadNode < Node
89
+ # Returns the list of parts for the full name of this constant.
90
+ # For example: [:Foo]
91
+ def full_name_parts
92
+ [name]
93
+ end
94
+
95
+ # Returns the full name of this constant. For example: "Foo"
96
+ def full_name
97
+ name.to_s
98
+ end
99
+ end
100
+
101
+ class ConstantPathNode < Node
102
+ # An error class raised when dynamic parts are found while computing a
103
+ # constant path's full name. For example:
104
+ # Foo::Bar::Baz -> does not raise because all parts of the constant path are
105
+ # simple constants
106
+ # var::Bar::Baz -> raises because the first part of the constant path is a
107
+ # local variable
108
+ class DynamicPartsInConstantPathError < StandardError; end
109
+
110
+ # Returns the list of parts for the full name of this constant path.
111
+ # For example: [:Foo, :Bar]
112
+ def full_name_parts
113
+ parts = [child.name]
114
+ current = parent
115
+
116
+ while current.is_a?(ConstantPathNode)
117
+ parts.unshift(current.child.name)
118
+ current = current.parent
119
+ end
120
+
121
+ if !current.is_a?(ConstantReadNode) && !current.nil?
122
+ raise DynamicPartsInConstantPathError, "Constant path contains dynamic parts. Cannot compute full name"
123
+ end
124
+
125
+ parts.unshift(current&.name || :"")
126
+ end
127
+
128
+ # Returns the full name of this constant path. For example: "Foo::Bar"
129
+ def full_name
130
+ full_name_parts.join("::")
131
+ end
132
+ end
133
+
134
+ class ConstantPathTargetNode < Node
135
+ # Returns the list of parts for the full name of this constant path.
136
+ # For example: [:Foo, :Bar]
137
+ def full_name_parts
138
+ parts = case parent
139
+ when ConstantPathNode, ConstantReadNode
140
+ parent.full_name_parts
141
+ when nil
142
+ [:""]
143
+ else
144
+ raise ConstantPathNode::DynamicPartsInConstantPathError,
145
+ "Constant path target contains dynamic parts. Cannot compute full name"
146
+ end
147
+
148
+ parts.push(child.name)
149
+ end
150
+
151
+ # Returns the full name of this constant path. For example: "Foo::Bar"
152
+ def full_name
153
+ full_name_parts.join("::")
154
+ end
155
+ end
156
+
157
+ class ConstantTargetNode < Node
158
+ # Returns the list of parts for the full name of this constant.
159
+ # For example: [:Foo]
160
+ def full_name_parts
161
+ [name]
162
+ end
163
+
164
+ # Returns the full name of this constant. For example: "Foo"
165
+ def full_name
166
+ name.to_s
167
+ end
168
+ end
169
+
170
+ class ParametersNode < Node
171
+ # Mirrors the Method#parameters method.
172
+ def signature
173
+ names = []
174
+
175
+ requireds.each do |param|
176
+ names << (param.is_a?(MultiTargetNode) ? [:req] : [:req, param.name])
177
+ end
178
+
179
+ optionals.each { |param| names << [:opt, param.name] }
180
+ names << [:rest, rest.name || :*] if rest
181
+
182
+ posts.each do |param|
183
+ names << (param.is_a?(MultiTargetNode) ? [:req] : [:req, param.name])
184
+ end
185
+
186
+ # Regardless of the order in which the keywords were defined, the required
187
+ # keywords always come first followed by the optional keywords.
188
+ keyopt = []
189
+ keywords.each do |param|
190
+ if param.is_a?(OptionalKeywordParameterNode)
191
+ keyopt << param
192
+ else
193
+ names << [:keyreq, param.name]
194
+ end
195
+ end
196
+
197
+ keyopt.each { |param| names << [:key, param.name] }
198
+
199
+ case keyword_rest
200
+ when ForwardingParameterNode
201
+ names.concat([[:rest, :*], [:keyrest, :**], [:block, :&]])
202
+ when KeywordRestParameterNode
203
+ names << [:keyrest, keyword_rest.name || :**]
204
+ when NoKeywordsParameterNode
205
+ names << [:nokey]
206
+ end
207
+
208
+ names << [:block, block.name || :&] if block
209
+ names
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prism
4
+ # This object is responsible for generating the output for the inspect method
5
+ # implementations of child nodes.
6
+ class NodeInspector # :nodoc:
7
+ attr_reader :prefix, :output
8
+
9
+ def initialize(prefix = "")
10
+ @prefix = prefix
11
+ @output = +""
12
+ end
13
+
14
+ # Appends a line to the output with the current prefix.
15
+ def <<(line)
16
+ output << "#{prefix}#{line}"
17
+ end
18
+
19
+ # This generates a string that is used as the header of the inspect output
20
+ # for any given node.
21
+ def header(node)
22
+ output = +"@ #{node.class.name.split("::").last} ("
23
+ output << "location: (#{node.location.start_line},#{node.location.start_column})-(#{node.location.end_line},#{node.location.end_column})"
24
+ output << ", newline: true" if node.newline?
25
+ output << ")\n"
26
+ output
27
+ end
28
+
29
+ # Generates a string that represents a list of nodes. It handles properly
30
+ # using the box drawing characters to make the output look nice.
31
+ def list(prefix, nodes)
32
+ output = +"(length: #{nodes.length})\n"
33
+ last_index = nodes.length - 1
34
+
35
+ nodes.each_with_index do |node, index|
36
+ pointer, preadd = (index == last_index) ? ["└── ", " "] : ["├── ", "│ "]
37
+ node_prefix = "#{prefix}#{preadd}"
38
+ output << node.inspect(NodeInspector.new(node_prefix)).sub(node_prefix, "#{prefix}#{pointer}")
39
+ end
40
+
41
+ output
42
+ end
43
+
44
+ # Generates a string that represents a location field on a node.
45
+ def location(value)
46
+ if value
47
+ "(#{value.start_line},#{value.start_column})-(#{value.end_line},#{value.end_column}) = #{value.slice.inspect}"
48
+ else
49
+ "∅"
50
+ end
51
+ end
52
+
53
+ # Generates a string that represents a child node.
54
+ def child_node(node, append)
55
+ node.inspect(child_inspector(append)).delete_prefix(prefix)
56
+ end
57
+
58
+ # Returns a new inspector that can be used to inspect a child node.
59
+ def child_inspector(append)
60
+ NodeInspector.new("#{prefix}#{append}")
61
+ end
62
+
63
+ # Returns the output as a string.
64
+ def to_str
65
+ output
66
+ end
67
+ end
68
+ end
data/lib/prism/pack.rb ADDED
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prism
4
+ # A parser for the pack template language.
5
+ module Pack
6
+ %i[
7
+ SPACE
8
+ COMMENT
9
+ INTEGER
10
+ UTF8
11
+ BER
12
+ FLOAT
13
+ STRING_SPACE_PADDED
14
+ STRING_NULL_PADDED
15
+ STRING_NULL_TERMINATED
16
+ STRING_MSB
17
+ STRING_LSB
18
+ STRING_HEX_HIGH
19
+ STRING_HEX_LOW
20
+ STRING_UU
21
+ STRING_MIME
22
+ STRING_BASE64
23
+ STRING_FIXED
24
+ STRING_POINTER
25
+ MOVE
26
+ BACK
27
+ NULL
28
+
29
+ UNSIGNED
30
+ SIGNED
31
+ SIGNED_NA
32
+
33
+ AGNOSTIC_ENDIAN
34
+ LITTLE_ENDIAN
35
+ BIG_ENDIAN
36
+ NATIVE_ENDIAN
37
+ ENDIAN_NA
38
+
39
+ SIZE_SHORT
40
+ SIZE_INT
41
+ SIZE_LONG
42
+ SIZE_LONG_LONG
43
+ SIZE_8
44
+ SIZE_16
45
+ SIZE_32
46
+ SIZE_64
47
+ SIZE_P
48
+ SIZE_NA
49
+
50
+ LENGTH_FIXED
51
+ LENGTH_MAX
52
+ LENGTH_RELATIVE
53
+ LENGTH_NA
54
+ ].each do |const|
55
+ const_set(const, const)
56
+ end
57
+
58
+ # A directive in the pack template language.
59
+ class Directive
60
+ # A symbol representing the version of Ruby.
61
+ attr_reader :version
62
+
63
+ # A symbol representing whether or not we are packing or unpacking.
64
+ attr_reader :variant
65
+
66
+ # A byteslice of the source string that this directive represents.
67
+ attr_reader :source
68
+
69
+ # The type of the directive.
70
+ attr_reader :type
71
+
72
+ # The type of signedness of the directive.
73
+ attr_reader :signed
74
+
75
+ # The type of endianness of the directive.
76
+ attr_reader :endian
77
+
78
+ # The size of the directive.
79
+ attr_reader :size
80
+
81
+ # The length type of this directive (used for integers).
82
+ attr_reader :length_type
83
+
84
+ # The length of this directive (used for integers).
85
+ attr_reader :length
86
+
87
+ # Initialize a new directive with the given values.
88
+ def initialize(version, variant, source, type, signed, endian, size, length_type, length)
89
+ @version = version
90
+ @variant = variant
91
+ @source = source
92
+ @type = type
93
+ @signed = signed
94
+ @endian = endian
95
+ @size = size
96
+ @length_type = length_type
97
+ @length = length
98
+ end
99
+
100
+ # The descriptions of the various types of endianness.
101
+ ENDIAN_DESCRIPTIONS = {
102
+ AGNOSTIC_ENDIAN: "agnostic",
103
+ LITTLE_ENDIAN: "little-endian (VAX)",
104
+ BIG_ENDIAN: "big-endian (network)",
105
+ NATIVE_ENDIAN: "native-endian",
106
+ ENDIAN_NA: "n/a"
107
+ }
108
+
109
+ # The descriptions of the various types of signedness.
110
+ SIGNED_DESCRIPTIONS = {
111
+ UNSIGNED: "unsigned",
112
+ SIGNED: "signed",
113
+ SIGNED_NA: "n/a"
114
+ }
115
+
116
+ # The descriptions of the various types of sizes.
117
+ SIZE_DESCRIPTIONS = {
118
+ SIZE_SHORT: "short",
119
+ SIZE_INT: "int-width",
120
+ SIZE_LONG: "long",
121
+ SIZE_LONG_LONG: "long long",
122
+ SIZE_8: "8-bit",
123
+ SIZE_16: "16-bit",
124
+ SIZE_32: "32-bit",
125
+ SIZE_64: "64-bit",
126
+ SIZE_P: "pointer-width"
127
+ }
128
+
129
+ # Provide a human-readable description of the directive.
130
+ def describe
131
+ case type
132
+ when SPACE
133
+ "whitespace"
134
+ when COMMENT
135
+ "comment"
136
+ when INTEGER
137
+ if size == SIZE_8
138
+ base = "#{SIGNED_DESCRIPTIONS[signed]} #{SIZE_DESCRIPTIONS[size]} integer"
139
+ else
140
+ base = "#{SIGNED_DESCRIPTIONS[signed]} #{SIZE_DESCRIPTIONS[size]} #{ENDIAN_DESCRIPTIONS[endian]} integer"
141
+ end
142
+ case length_type
143
+ when LENGTH_FIXED
144
+ if length > 1
145
+ base + ", x#{length}"
146
+ else
147
+ base
148
+ end
149
+ when LENGTH_MAX
150
+ base + ", as many as possible"
151
+ end
152
+ when UTF8
153
+ "UTF-8 character"
154
+ when BER
155
+ "BER-compressed integer"
156
+ when FLOAT
157
+ "#{SIZE_DESCRIPTIONS[size]} #{ENDIAN_DESCRIPTIONS[endian]} float"
158
+ when STRING_SPACE_PADDED
159
+ "arbitrary binary string (space padded)"
160
+ when STRING_NULL_PADDED
161
+ "arbitrary binary string (null padded, count is width)"
162
+ when STRING_NULL_TERMINATED
163
+ "arbitrary binary string (null padded, count is width), except that null is added with *"
164
+ when STRING_MSB
165
+ "bit string (MSB first)"
166
+ when STRING_LSB
167
+ "bit string (LSB first)"
168
+ when STRING_HEX_HIGH
169
+ "hex string (high nibble first)"
170
+ when STRING_HEX_LOW
171
+ "hex string (low nibble first)"
172
+ when STRING_UU
173
+ "UU-encoded string"
174
+ when STRING_MIME
175
+ "quoted printable, MIME encoding"
176
+ when STRING_BASE64
177
+ "base64 encoded string"
178
+ when STRING_FIXED
179
+ "pointer to a structure (fixed-length string)"
180
+ when STRING_POINTER
181
+ "pointer to a null-terminated string"
182
+ when MOVE
183
+ "move to absolute position"
184
+ when BACK
185
+ "back up a byte"
186
+ when NULL
187
+ "null byte"
188
+ else
189
+ raise
190
+ end
191
+ end
192
+ end
193
+
194
+ # The result of parsing a pack template.
195
+ class Format
196
+ # A list of the directives in the template.
197
+ attr_reader :directives
198
+
199
+ # The encoding of the template.
200
+ attr_reader :encoding
201
+
202
+ # Create a new Format with the given directives and encoding.
203
+ def initialize(directives, encoding)
204
+ @directives = directives
205
+ @encoding = encoding
206
+ end
207
+
208
+ # Provide a human-readable description of the format.
209
+ def describe
210
+ source_width = directives.map { |d| d.source.inspect.length }.max
211
+ directive_lines = directives.map do |directive|
212
+ if directive.type == SPACE
213
+ source = directive.source.inspect
214
+ else
215
+ source = directive.source
216
+ end
217
+ " #{source.ljust(source_width)} #{directive.describe}"
218
+ end
219
+
220
+ (["Directives:"] + directive_lines + ["Encoding:", " #{encoding}"]).join("\n")
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prism
4
+ class ParseResult
5
+ # When we've parsed the source, we have both the syntax tree and the list of
6
+ # comments that we found in the source. This class is responsible for
7
+ # walking the tree and finding the nearest location to attach each comment.
8
+ #
9
+ # It does this by first finding the nearest locations to each comment.
10
+ # Locations can either come from nodes directly or from location fields on
11
+ # nodes. For example, a `ClassNode` has an overall location encompassing the
12
+ # entire class, but it also has a location for the `class` keyword.
13
+ #
14
+ # Once the nearest locations are found, it determines which one to attach
15
+ # to. If it's a trailing comment (a comment on the same line as other source
16
+ # code), it will favor attaching to the nearest location that occurs before
17
+ # the comment. Otherwise it will favor attaching to the nearest location
18
+ # that is after the comment.
19
+ class Comments
20
+ # A target for attaching comments that is based on a specific node's
21
+ # location.
22
+ class NodeTarget # :nodoc:
23
+ attr_reader :node
24
+
25
+ def initialize(node)
26
+ @node = node
27
+ end
28
+
29
+ def start_offset
30
+ node.location.start_offset
31
+ end
32
+
33
+ def end_offset
34
+ node.location.end_offset
35
+ end
36
+
37
+ def encloses?(comment)
38
+ start_offset <= comment.location.start_offset &&
39
+ comment.location.end_offset <= end_offset
40
+ end
41
+
42
+ def <<(comment)
43
+ node.location.comments << comment
44
+ end
45
+ end
46
+
47
+ # A target for attaching comments that is based on a location field on a
48
+ # node. For example, the `end` token of a ClassNode.
49
+ class LocationTarget # :nodoc:
50
+ attr_reader :location
51
+
52
+ def initialize(location)
53
+ @location = location
54
+ end
55
+
56
+ def start_offset
57
+ location.start_offset
58
+ end
59
+
60
+ def end_offset
61
+ location.end_offset
62
+ end
63
+
64
+ def encloses?(comment)
65
+ false
66
+ end
67
+
68
+ def <<(comment)
69
+ location.comments << comment
70
+ end
71
+ end
72
+
73
+ # The parse result that we are attaching comments to.
74
+ attr_reader :parse_result
75
+
76
+ # Create a new Comments object that will attach comments to the given
77
+ # parse result.
78
+ def initialize(parse_result)
79
+ @parse_result = parse_result
80
+ end
81
+
82
+ # Attach the comments to their respective locations in the tree by
83
+ # mutating the parse result.
84
+ def attach!
85
+ parse_result.comments.each do |comment|
86
+ preceding, enclosing, following = nearest_targets(parse_result.value, comment)
87
+ target =
88
+ if comment.trailing?
89
+ preceding || following || enclosing || NodeTarget.new(parse_result.value)
90
+ else
91
+ # If a comment exists on its own line, prefer a leading comment.
92
+ following || preceding || enclosing || NodeTarget.new(parse_result.value)
93
+ end
94
+
95
+ target << comment
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ # Responsible for finding the nearest targets to the given comment within
102
+ # the context of the given encapsulating node.
103
+ def nearest_targets(node, comment)
104
+ comment_start = comment.location.start_offset
105
+ comment_end = comment.location.end_offset
106
+
107
+ targets = []
108
+ node.comment_targets.map do |value|
109
+ case value
110
+ when StatementsNode
111
+ targets.concat(value.body.map { |node| NodeTarget.new(node) })
112
+ when Node
113
+ targets << NodeTarget.new(value)
114
+ when Location
115
+ targets << LocationTarget.new(value)
116
+ end
117
+ end
118
+
119
+ targets.sort_by!(&:start_offset)
120
+ preceding = nil
121
+ following = nil
122
+
123
+ left = 0
124
+ right = targets.length
125
+
126
+ # This is a custom binary search that finds the nearest nodes to the
127
+ # given comment. When it finds a node that completely encapsulates the
128
+ # comment, it recurses downward into the tree.
129
+ while left < right
130
+ middle = (left + right) / 2
131
+ target = targets[middle]
132
+
133
+ target_start = target.start_offset
134
+ target_end = target.end_offset
135
+
136
+ if target.encloses?(comment)
137
+ # The comment is completely contained by this target. Abandon the
138
+ # binary search at this level.
139
+ return nearest_targets(target.node, comment)
140
+ end
141
+
142
+ if target_end <= comment_start
143
+ # This target falls completely before the comment. Because we will
144
+ # never consider this target or any targets before it again, this
145
+ # target must be the closest preceding target we have encountered so
146
+ # far.
147
+ preceding = target
148
+ left = middle + 1
149
+ next
150
+ end
151
+
152
+ if comment_end <= target_start
153
+ # This target falls completely after the comment. Because we will
154
+ # never consider this target or any targets after it again, this
155
+ # target must be the closest following target we have encountered so
156
+ # far.
157
+ following = target
158
+ right = middle
159
+ next
160
+ end
161
+
162
+ # This should only happen if there is a bug in this parser.
163
+ raise "Comment location overlaps with a target location"
164
+ end
165
+
166
+ [preceding, NodeTarget.new(node), following]
167
+ end
168
+ end
169
+
170
+ private_constant :Comments
171
+
172
+ # Attach the list of comments to their respective locations in the tree.
173
+ def attach_comments!
174
+ Comments.new(self).attach!
175
+ end
176
+ end
177
+ end