protostar 0.1.0.beta1

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 (223) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +647 -0
  5. data/THIRD_PARTY_NOTICES.md +61 -0
  6. data/exe/protoc-gen-protostar +7 -0
  7. data/exe/protostar +7 -0
  8. data/lib/protostar/builder/generator.rb +71 -0
  9. data/lib/protostar/builder/sink.rb +35 -0
  10. data/lib/protostar/builder.rb +965 -0
  11. data/lib/protostar/cli.rb +267 -0
  12. data/lib/protostar/codec/decode_generator.rb +199 -0
  13. data/lib/protostar/codec/decoder.rb +296 -0
  14. data/lib/protostar/codec/encode_generator.rb +224 -0
  15. data/lib/protostar/codec/encoder.rb +121 -0
  16. data/lib/protostar/codec/sizer.rb +91 -0
  17. data/lib/protostar/codec.rb +15 -0
  18. data/lib/protostar/compiler/ast.rb +193 -0
  19. data/lib/protostar/compiler/emitters/descriptor_set.rb +57 -0
  20. data/lib/protostar/compiler/emitters/google_ruby.rb +103 -0
  21. data/lib/protostar/compiler/emitters/grpc.rb +175 -0
  22. data/lib/protostar/compiler/emitters/protostar_rbs.rb +252 -0
  23. data/lib/protostar/compiler/emitters/rbs.rb +394 -0
  24. data/lib/protostar/compiler/emitters/ruby.rb +503 -0
  25. data/lib/protostar/compiler/emitters.rb +268 -0
  26. data/lib/protostar/compiler/include/LICENSE +32 -0
  27. data/lib/protostar/compiler/include/google/protobuf/any.proto +106 -0
  28. data/lib/protostar/compiler/include/google/protobuf/api.proto +229 -0
  29. data/lib/protostar/compiler/include/google/protobuf/compiler/plugin.proto +180 -0
  30. data/lib/protostar/compiler/include/google/protobuf/cpp_features.proto +87 -0
  31. data/lib/protostar/compiler/include/google/protobuf/descriptor.proto +1532 -0
  32. data/lib/protostar/compiler/include/google/protobuf/duration.proto +115 -0
  33. data/lib/protostar/compiler/include/google/protobuf/empty.proto +51 -0
  34. data/lib/protostar/compiler/include/google/protobuf/field_mask.proto +243 -0
  35. data/lib/protostar/compiler/include/google/protobuf/java_features.proto +132 -0
  36. data/lib/protostar/compiler/include/google/protobuf/source_context.proto +48 -0
  37. data/lib/protostar/compiler/include/google/protobuf/struct.proto +111 -0
  38. data/lib/protostar/compiler/include/google/protobuf/timestamp.proto +145 -0
  39. data/lib/protostar/compiler/include/google/protobuf/type.proto +217 -0
  40. data/lib/protostar/compiler/include/google/protobuf/wrappers.proto +157 -0
  41. data/lib/protostar/compiler/lexer.rb +659 -0
  42. data/lib/protostar/compiler/linker.rb +894 -0
  43. data/lib/protostar/compiler/option_interpreter.rb +363 -0
  44. data/lib/protostar/compiler/parser.rb +1696 -0
  45. data/lib/protostar/compiler/retention.rb +104 -0
  46. data/lib/protostar/compiler/run.rb +184 -0
  47. data/lib/protostar/compiler/source_info.rb +161 -0
  48. data/lib/protostar/compiler/symbol_table.rb +223 -0
  49. data/lib/protostar/compiler/text_format.rb +340 -0
  50. data/lib/protostar/compiler/validator.rb +944 -0
  51. data/lib/protostar/compiler.rb +304 -0
  52. data/lib/protostar/diagnostics/codes.rb +1307 -0
  53. data/lib/protostar/diagnostics/formats.rb +64 -0
  54. data/lib/protostar/diagnostics/renderer.rb +240 -0
  55. data/lib/protostar/diagnostics/source.rb +153 -0
  56. data/lib/protostar/diagnostics.rb +117 -0
  57. data/lib/protostar/dsl.rb +367 -0
  58. data/lib/protostar/enum.rb +70 -0
  59. data/lib/protostar/errors.rb +23 -0
  60. data/lib/protostar/google/protobuf/any_pb.rb +90 -0
  61. data/lib/protostar/google/protobuf/api_pb.rb +199 -0
  62. data/lib/protostar/google/protobuf/compiler/plugin_pb.rb +168 -0
  63. data/lib/protostar/google/protobuf/descriptor_pb.rb +1239 -0
  64. data/lib/protostar/google/protobuf/duration_pb.rb +99 -0
  65. data/lib/protostar/google/protobuf/empty_pb.rb +36 -0
  66. data/lib/protostar/google/protobuf/field_mask_pb.rb +228 -0
  67. data/lib/protostar/google/protobuf/source_context_pb.rb +33 -0
  68. data/lib/protostar/google/protobuf/struct_pb.rb +91 -0
  69. data/lib/protostar/google/protobuf/timestamp_pb.rb +129 -0
  70. data/lib/protostar/google/protobuf/type_pb.rb +202 -0
  71. data/lib/protostar/google/protobuf/wrappers_pb.rb +125 -0
  72. data/lib/protostar/google_protobuf_shim.rb +24 -0
  73. data/lib/protostar/json/encoder.rb +340 -0
  74. data/lib/protostar/json/errors.rb +34 -0
  75. data/lib/protostar/json/parser.rb +437 -0
  76. data/lib/protostar/json/path.rb +39 -0
  77. data/lib/protostar/json.rb +95 -0
  78. data/lib/protostar/lint/command.rb +197 -0
  79. data/lib/protostar/lint/config.rb +98 -0
  80. data/lib/protostar/lint/context.rb +126 -0
  81. data/lib/protostar/lint/directives.rb +56 -0
  82. data/lib/protostar/lint/fixer.rb +165 -0
  83. data/lib/protostar/lint/naming.rb +54 -0
  84. data/lib/protostar/lint/rule.rb +120 -0
  85. data/lib/protostar/lint/rules/comment_enum.rb +31 -0
  86. data/lib/protostar/lint/rules/comment_enum_value.rb +32 -0
  87. data/lib/protostar/lint/rules/comment_field.rb +33 -0
  88. data/lib/protostar/lint/rules/comment_message.rb +31 -0
  89. data/lib/protostar/lint/rules/comment_rpc.rb +31 -0
  90. data/lib/protostar/lint/rules/comment_service.rb +31 -0
  91. data/lib/protostar/lint/rules/edition_recommended.rb +31 -0
  92. data/lib/protostar/lint/rules/enum_first_value_zero.rb +34 -0
  93. data/lib/protostar/lint/rules/enum_pascal_case.rb +25 -0
  94. data/lib/protostar/lint/rules/enum_value_prefix.rb +38 -0
  95. data/lib/protostar/lint/rules/enum_value_upper_snake_case.rb +28 -0
  96. data/lib/protostar/lint/rules/enum_zero_value_suffix.rb +41 -0
  97. data/lib/protostar/lint/rules/field_lower_snake_case.rb +34 -0
  98. data/lib/protostar/lint/rules/field_not_required.rb +39 -0
  99. data/lib/protostar/lint/rules/field_number_reuse_risk.rb +45 -0
  100. data/lib/protostar/lint/rules/field_numbers_hot_range.rb +44 -0
  101. data/lib/protostar/lint/rules/file_lower_snake_case.rb +27 -0
  102. data/lib/protostar/lint/rules/import_no_public.rb +29 -0
  103. data/lib/protostar/lint/rules/import_no_weak.rb +28 -0
  104. data/lib/protostar/lint/rules/import_used.rb +33 -0
  105. data/lib/protostar/lint/rules/message_pascal_case.rb +27 -0
  106. data/lib/protostar/lint/rules/no_deprecated_features.rb +50 -0
  107. data/lib/protostar/lint/rules/no_groups.rb +34 -0
  108. data/lib/protostar/lint/rules/oneof_lower_snake_case.rb +27 -0
  109. data/lib/protostar/lint/rules/package_defined.rb +28 -0
  110. data/lib/protostar/lint/rules/package_directory_match.rb +33 -0
  111. data/lib/protostar/lint/rules/package_lower_snake_case.rb +28 -0
  112. data/lib/protostar/lint/rules/package_same_directory.rb +39 -0
  113. data/lib/protostar/lint/rules/package_version_suffix.rb +31 -0
  114. data/lib/protostar/lint/rules/reserved_names_with_numbers.rb +44 -0
  115. data/lib/protostar/lint/rules/rpc_pascal_case.rb +25 -0
  116. data/lib/protostar/lint/rules/rpc_request_response_unique.rb +40 -0
  117. data/lib/protostar/lint/rules/rpc_request_standard_name.rb +32 -0
  118. data/lib/protostar/lint/rules/rpc_response_standard_name.rb +31 -0
  119. data/lib/protostar/lint/rules/service_pascal_case.rb +25 -0
  120. data/lib/protostar/lint/rules/service_suffix.rb +28 -0
  121. data/lib/protostar/lint/rules/syntax_specified.rb +35 -0
  122. data/lib/protostar/lint/runner.rb +135 -0
  123. data/lib/protostar/lint.rb +122 -0
  124. data/lib/protostar/message/accessors.rb +86 -0
  125. data/lib/protostar/message/coercion.rb +171 -0
  126. data/lib/protostar/message/field_info.rb +145 -0
  127. data/lib/protostar/message/reflection.rb +149 -0
  128. data/lib/protostar/message.rb +572 -0
  129. data/lib/protostar/plugin.rb +94 -0
  130. data/lib/protostar/schema/default_value.rb +113 -0
  131. data/lib/protostar/schema/descriptor.rb +41 -0
  132. data/lib/protostar/schema/draft/copy.rb +88 -0
  133. data/lib/protostar/schema/draft/resolution.rb +177 -0
  134. data/lib/protostar/schema/draft.rb +779 -0
  135. data/lib/protostar/schema/enum.rb +106 -0
  136. data/lib/protostar/schema/extension_range.rb +37 -0
  137. data/lib/protostar/schema/features.rb +153 -0
  138. data/lib/protostar/schema/field.rb +201 -0
  139. data/lib/protostar/schema/file.rb +217 -0
  140. data/lib/protostar/schema/freezer.rb +61 -0
  141. data/lib/protostar/schema/message.rb +104 -0
  142. data/lib/protostar/schema/oneof.rb +38 -0
  143. data/lib/protostar/schema/options.rb +367 -0
  144. data/lib/protostar/schema/pool.rb +179 -0
  145. data/lib/protostar/schema/proto.rb +576 -0
  146. data/lib/protostar/schema/resolver.rb +118 -0
  147. data/lib/protostar/schema/service.rb +89 -0
  148. data/lib/protostar/schema/source_info.rb +53 -0
  149. data/lib/protostar/schema.rb +65 -0
  150. data/lib/protostar/short_names/any.rb +7 -0
  151. data/lib/protostar/short_names/duration.rb +7 -0
  152. data/lib/protostar/short_names/empty.rb +7 -0
  153. data/lib/protostar/short_names/field_mask.rb +7 -0
  154. data/lib/protostar/short_names/struct.rb +9 -0
  155. data/lib/protostar/short_names/timestamp.rb +7 -0
  156. data/lib/protostar/short_names/wrappers.rb +15 -0
  157. data/lib/protostar/text/extensions.rb +114 -0
  158. data/lib/protostar/text/parser.rb +439 -0
  159. data/lib/protostar/text/printer.rb +304 -0
  160. data/lib/protostar/text/tokenizer.rb +116 -0
  161. data/lib/protostar/text.rb +67 -0
  162. data/lib/protostar/types.rb +152 -0
  163. data/lib/protostar/unknown_field.rb +58 -0
  164. data/lib/protostar/version.rb +5 -0
  165. data/lib/protostar/view/generator.rb +298 -0
  166. data/lib/protostar/view/map.rb +194 -0
  167. data/lib/protostar/view/repeated.rb +143 -0
  168. data/lib/protostar/view/scan.rb +183 -0
  169. data/lib/protostar/view.rb +283 -0
  170. data/lib/protostar/well_known/any.rb +52 -0
  171. data/lib/protostar/well_known/duration.rb +53 -0
  172. data/lib/protostar/well_known/list_value.rb +63 -0
  173. data/lib/protostar/well_known/struct.rb +55 -0
  174. data/lib/protostar/well_known/timestamp.rb +64 -0
  175. data/lib/protostar/well_known/value.rb +60 -0
  176. data/lib/protostar/well_known.rb +38 -0
  177. data/lib/protostar/wire/io_source.rb +40 -0
  178. data/lib/protostar/wire/reader.rb +254 -0
  179. data/lib/protostar/wire/writer.rb +201 -0
  180. data/lib/protostar/wire.rb +96 -0
  181. data/lib/protostar.rb +95 -0
  182. data/shims/google-protobuf/lib/google/protobuf/any_pb.rb +17 -0
  183. data/shims/google-protobuf/lib/google/protobuf/api_pb.rb +22 -0
  184. data/shims/google-protobuf/lib/google/protobuf/compiler/plugin_pb.rb +25 -0
  185. data/shims/google-protobuf/lib/google/protobuf/descriptor_pb.rb +72 -0
  186. data/shims/google-protobuf/lib/google/protobuf/descriptor_pool.rb +164 -0
  187. data/shims/google-protobuf/lib/google/protobuf/descriptors.rb +553 -0
  188. data/shims/google-protobuf/lib/google/protobuf/duration_pb.rb +17 -0
  189. data/shims/google-protobuf/lib/google/protobuf/empty_pb.rb +17 -0
  190. data/shims/google-protobuf/lib/google/protobuf/field_mask_pb.rb +17 -0
  191. data/shims/google-protobuf/lib/google/protobuf/internal/convert.rb +180 -0
  192. data/shims/google-protobuf/lib/google/protobuf/internal/extensions.rb +154 -0
  193. data/shims/google-protobuf/lib/google/protobuf/internal/json.rb +104 -0
  194. data/shims/google-protobuf/lib/google/protobuf/internal/well_known.rb +140 -0
  195. data/shims/google-protobuf/lib/google/protobuf/map.rb +216 -0
  196. data/shims/google-protobuf/lib/google/protobuf/message.rb +544 -0
  197. data/shims/google-protobuf/lib/google/protobuf/message_exts.rb +41 -0
  198. data/shims/google-protobuf/lib/google/protobuf/plugin_pb.rb +4 -0
  199. data/shims/google-protobuf/lib/google/protobuf/repeated_field.rb +366 -0
  200. data/shims/google-protobuf/lib/google/protobuf/source_context_pb.rb +17 -0
  201. data/shims/google-protobuf/lib/google/protobuf/struct_pb.rb +20 -0
  202. data/shims/google-protobuf/lib/google/protobuf/timestamp_pb.rb +17 -0
  203. data/shims/google-protobuf/lib/google/protobuf/type_pb.rb +27 -0
  204. data/shims/google-protobuf/lib/google/protobuf/well_known_types.rb +37 -0
  205. data/shims/google-protobuf/lib/google/protobuf/wrappers_pb.rb +25 -0
  206. data/shims/google-protobuf/lib/google/protobuf.rb +107 -0
  207. data/shims/google-protobuf/lib/google/protobuf_ffi.rb +5 -0
  208. data/shims/google-protobuf/lib/google/protobuf_native.rb +5 -0
  209. data/sig/protostar/google/protobuf/any_pb.rbs +77 -0
  210. data/sig/protostar/google/protobuf/api_pb.rbs +242 -0
  211. data/sig/protostar/google/protobuf/compiler/plugin_pb.rbs +264 -0
  212. data/sig/protostar/google/protobuf/descriptor_pb.rbs +2220 -0
  213. data/sig/protostar/google/protobuf/duration_pb.rbs +82 -0
  214. data/sig/protostar/google/protobuf/empty_pb.rbs +18 -0
  215. data/sig/protostar/google/protobuf/field_mask_pb.rbs +170 -0
  216. data/sig/protostar/google/protobuf/source_context_pb.rbs +24 -0
  217. data/sig/protostar/google/protobuf/struct_pb.rbs +135 -0
  218. data/sig/protostar/google/protobuf/timestamp_pb.rbs +102 -0
  219. data/sig/protostar/google/protobuf/type_pb.rbs +304 -0
  220. data/sig/protostar/google/protobuf/wrappers_pb.rbs +169 -0
  221. data/sig/protostar/lint.rbs +216 -0
  222. data/sig/protostar.rbs +326 -0
  223. metadata +366 -0
@@ -0,0 +1,659 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protostar
4
+ class Compiler
5
+ # protoc's tokenizer (`io/tokenizer.cc`), byte for byte: the same token
6
+ # boundaries, the same 0-based lines and columns (tabs advance to the next
7
+ # multiple of 8), the same error messages and the same comment attachment
8
+ # in {#next_with_comments}.
9
+ #
10
+ # @api private
11
+ class Lexer
12
+ # A token. The lexer reuses its current token as it advances, as
13
+ # protoc's Tokenizer does, and copies it into {Lexer#previous}.
14
+ class Token
15
+ # @return [Symbol] :start, :identifier, :integer, :float, :string, :symbol, :end,
16
+ # and :whitespace or :newline when asked for
17
+ attr_accessor :type
18
+ # @return [String] the source text, quotes and escapes included for strings
19
+ attr_accessor :text
20
+ # @return [Integer] 0-based
21
+ attr_accessor :line
22
+ # @return [Integer] 0-based, a tab advancing to the next multiple of 8
23
+ attr_accessor :column
24
+ # @return [Integer] 0-based column just after the token
25
+ attr_accessor :end_column
26
+
27
+ def initialize(type:, text:, line:, column:, end_column:)
28
+ @type = type
29
+ @text = text
30
+ @line = line
31
+ @column = column
32
+ @end_column = end_column
33
+ end
34
+
35
+ # @return [Array] type, text, line, column and end column
36
+ def to_a
37
+ [@type, @text, @line, @column, @end_column]
38
+ end
39
+
40
+ def inspect
41
+ "#<#{self.class.name} #{to_a.inspect}>"
42
+ end
43
+ end
44
+
45
+ TAB_WIDTH = 8
46
+
47
+ LETTER = Array.new(256) { |b| b.between?(97, 122) || b.between?(65, 90) || b == 95 }.freeze
48
+ DIGIT = Array.new(256) { |b| b.between?(48, 57) }.freeze
49
+ ALNUM = Array.new(256) { |b| LETTER[b] || DIGIT[b] }.freeze
50
+ OCTAL = Array.new(256) { |b| b.between?(48, 55) }.freeze
51
+ HEX = Array.new(256) { |b| DIGIT[b] || b.between?(97, 102) || b.between?(65, 70) }.freeze
52
+ # \t \n \v \f \r and space
53
+ WHITESPACE = Array.new(256) { |b| b == 32 || b.between?(9, 13) }.freeze
54
+ WHITESPACE_NO_NEWLINE = Array.new(256) { |b| WHITESPACE[b] && b != 10 }.freeze
55
+ UNPRINTABLE = Array.new(256) { |b| b >= 1 && b < 32 }.freeze
56
+ ESCAPE = Array.new(256) { |b| 'abfnrtv\\?\'"'.include?(b.chr) }.freeze
57
+
58
+ ESCAPES = {
59
+ "a" => 7, "b" => 8, "f" => 12, "n" => 10, "r" => 13, "t" => 9, "v" => 11,
60
+ "\\" => 92, "?" => 63, "'" => 39, "\"" => 34
61
+ }.freeze
62
+
63
+ # @return [Token]
64
+ attr_reader :current, :previous
65
+ attr_accessor :report_whitespace, :report_newlines
66
+
67
+ # @param source [String] the file's bytes
68
+ # @param errors [#call] receives (line, column, message) for every error
69
+ def initialize(source, errors)
70
+ @src = source.b
71
+ @size = @src.bytesize
72
+ @errors = errors
73
+ @pos = 0
74
+ @line = 0
75
+ @column = 0
76
+ @read_error = @size == 0
77
+ @char = (@size == 0) ? 0 : @src.getbyte(0)
78
+ @record_target = nil
79
+ @record_start = -1
80
+ @report_whitespace = false
81
+ @report_newlines = false
82
+ @current = Token.new(type: :start, text: +"", line: 0, column: 0, end_column: 0)
83
+ @previous = @current.dup
84
+ end
85
+
86
+ # Advances to the next token, skipping whitespace and comments
87
+ # @return [Boolean] false at the end of input
88
+ def next
89
+ @previous = @current.dup
90
+ until @read_error
91
+ start_token
92
+ report = try_consume_whitespace || try_consume_newline
93
+ end_token
94
+ return true if report
95
+
96
+ case try_consume_comment_start
97
+ when :line
98
+ consume_line_comment(nil)
99
+ next
100
+ when :block
101
+ consume_block_comment(nil)
102
+ next
103
+ when :slash
104
+ return true
105
+ end
106
+
107
+ break if @read_error
108
+
109
+ if UNPRINTABLE[@char] || @char == 0
110
+ add_error("Invalid control characters encountered in text.")
111
+ next_char
112
+ while try_consume_one(UNPRINTABLE) || (!@read_error && try_consume(0))
113
+ end
114
+ else
115
+ start_token
116
+ if try_consume_one(LETTER)
117
+ consume_zero_or_more(ALNUM)
118
+ @current.type = :identifier
119
+ elsif try_consume(48) # 0
120
+ @current.type = consume_number(true, false)
121
+ elsif try_consume(46) # .
122
+ if try_consume_one(DIGIT)
123
+ if @previous.type == :identifier && @current.line == @previous.line &&
124
+ @current.column == @previous.end_column
125
+ @errors.call(@line, @column - 2, "Need space between identifier and decimal point.")
126
+ end
127
+ @current.type = consume_number(false, true)
128
+ else
129
+ @current.type = :symbol
130
+ end
131
+ elsif try_consume_one(DIGIT)
132
+ @current.type = consume_number(false, false)
133
+ elsif try_consume(34) # "
134
+ consume_string(34)
135
+ @current.type = :string
136
+ elsif try_consume(39) # '
137
+ consume_string(39)
138
+ @current.type = :string
139
+ else
140
+ consume_symbol
141
+ @current.type = :symbol
142
+ end
143
+ end_token
144
+ return true
145
+ end
146
+ end
147
+
148
+ @current.type = :end
149
+ @current.text = +""
150
+ @current.line = @line
151
+ @current.column = @column
152
+ @current.end_column = @column
153
+ false
154
+ end
155
+
156
+ # Like {#next}, but collects the comments around the token boundary the
157
+ # way protoc attaches them to declarations
158
+ # @param prev_trailing [String, nil] receives the previous token's trailing comment
159
+ # @param detached [Array<String>, nil] receives detached comments
160
+ # @param next_leading [String, nil] receives the next token's leading comment
161
+ # @return [Boolean]
162
+ def next_with_comments(prev_trailing, detached, next_leading)
163
+ collector = CommentCollector.new(prev_trailing, detached, next_leading)
164
+ prev_line = @line
165
+ trailing_comment_end_line = -1
166
+
167
+ if @current.type == :start
168
+ if try_consume(0xEF)
169
+ unless try_consume(0xBB) && try_consume(0xBF)
170
+ add_error("Proto file starts with 0xEF but not UTF-8 BOM. Only UTF-8 is accepted for proto file.")
171
+ collector.finish
172
+ return false
173
+ end
174
+ end
175
+ collector.detach_from_prev
176
+ prev_line = -1
177
+ else
178
+ consume_zero_or_more(WHITESPACE_NO_NEWLINE)
179
+ case try_consume_comment_start
180
+ when :line
181
+ trailing_comment_end_line = @line
182
+ consume_line_comment(collector.buffer_for_line_comment)
183
+ collector.flush
184
+ when :block
185
+ consume_block_comment(collector.buffer_for_block_comment)
186
+ trailing_comment_end_line = @line
187
+ consume_zero_or_more(WHITESPACE_NO_NEWLINE)
188
+ collector.flush
189
+ when :slash
190
+ collector.finish
191
+ return true
192
+ else
193
+ unless try_consume(10)
194
+ collector.finish
195
+ return self.next
196
+ end
197
+ end
198
+ end
199
+
200
+ loop do
201
+ consume_zero_or_more(WHITESPACE_NO_NEWLINE)
202
+ case try_consume_comment_start
203
+ when :line
204
+ consume_line_comment(collector.buffer_for_line_comment)
205
+ when :block
206
+ consume_block_comment(collector.buffer_for_block_comment)
207
+ consume_zero_or_more(WHITESPACE_NO_NEWLINE)
208
+ try_consume(10)
209
+ when :slash
210
+ collector.finish
211
+ return true
212
+ else
213
+ if try_consume(10)
214
+ collector.flush
215
+ collector.detach_from_prev
216
+ else
217
+ result = self.next
218
+ text = @current.text
219
+ collector.flush if !result || text == "}" || text == "]" || text == ")"
220
+ if result && (prev_line == @line || trailing_comment_end_line == @line)
221
+ collector.maybe_detach_comment
222
+ end
223
+ collector.finish
224
+ return result
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ # protoc's Tokenizer::ParseInteger: decimal, 0x hex or leading-zero octal
231
+ # @return [Integer, nil] nil when malformed or above `max`
232
+ def self.parse_integer(text, max)
233
+ base = 10
234
+ digits = text
235
+ if text.start_with?("0x", "0X")
236
+ base = 16
237
+ digits = text[2..]
238
+ elsif text.start_with?("0")
239
+ base = 8
240
+ end
241
+ valid = case base
242
+ when 16 then digits.match?(/\A\h*\z/)
243
+ when 8 then digits.match?(/\A[0-7]*\z/)
244
+ else digits.match?(/\A[0-9]*\z/)
245
+ end
246
+ return nil unless valid
247
+ value = digits.empty? ? 0 : digits.to_i(base)
248
+ (value > max) ? nil : value
249
+ end
250
+
251
+ # protoc's Tokenizer::ParseFloat over text the tokenizer accepted as a number
252
+ # @return [Float]
253
+ def self.parse_float(text)
254
+ m = text.match(/\A(\d*)(?:\.(\d*))?(?:[eE]([-+]?\d+))?/)
255
+ int = m[1].empty? ? "0" : m[1]
256
+ frac = (m[2].nil? || m[2].empty?) ? "0" : m[2]
257
+ Float("#{int}.#{frac}e#{m[3] || "0"}")
258
+ end
259
+
260
+ # Whether strtod would consume all of `text` as a float, as
261
+ # Tokenizer::TryParseFloat checks
262
+ def self.float_text?(text)
263
+ text.match?(/\A(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?[fF]?\z/)
264
+ end
265
+
266
+ # protoc's Tokenizer::ParseStringAppend: the bytes a string token denotes
267
+ # @param text [String] token text including the quotes
268
+ # @return [String] binary
269
+ def self.parse_string(text, out = "".b)
270
+ bytes = text.b
271
+ quote = bytes.getbyte(0)
272
+ n = bytes.bytesize
273
+ i = 1
274
+ while i < n
275
+ b = bytes.getbyte(i)
276
+ if b == 92 && i + 1 < n # backslash
277
+ i += 1
278
+ c = bytes.getbyte(i)
279
+ if OCTAL[c]
280
+ code = c - 48
281
+ if i + 1 < n && OCTAL[bytes.getbyte(i + 1)]
282
+ i += 1
283
+ code = code * 8 + bytes.getbyte(i) - 48
284
+ end
285
+ if i + 1 < n && OCTAL[bytes.getbyte(i + 1)]
286
+ i += 1
287
+ code = code * 8 + bytes.getbyte(i) - 48
288
+ end
289
+ out << (code & 0xFF)
290
+ elsif c == 120 || c == 88 # x X
291
+ code = 0
292
+ if i + 1 < n && HEX[bytes.getbyte(i + 1)]
293
+ i += 1
294
+ code = bytes.getbyte(i).chr.to_i(16)
295
+ end
296
+ if i + 1 < n && HEX[bytes.getbyte(i + 1)]
297
+ i += 1
298
+ code = code * 16 + bytes.getbyte(i).chr.to_i(16)
299
+ end
300
+ out << code
301
+ elsif c == 117 || c == 85 # u U
302
+ len = (c == 117) ? 4 : 8
303
+ hex = bytes.byteslice(i + 1, len)
304
+ if hex.nil? || hex.bytesize < len || !hex.match?(/\A\h+\z/)
305
+ out << c
306
+ else
307
+ code = hex.to_i(16)
308
+ i += len
309
+ if code >= 0xD800 && code < 0xDC00 && bytes.byteslice(i + 1, 2) == "\\u"
310
+ trail = bytes.byteslice(i + 3, 4)
311
+ if trail&.match?(/\A\h{4}\z/) && (t = trail.to_i(16)) >= 0xDC00 && t < 0xE000
312
+ code = 0x10000 + (((code - 0xD800) << 10) | (t - 0xDC00))
313
+ i += 6
314
+ end
315
+ end
316
+ append_utf8(code, out)
317
+ end
318
+ else
319
+ out << (ESCAPES[c.chr] || 63)
320
+ end
321
+ elsif b == quote && i == n - 1
322
+ # closing quote
323
+ else
324
+ out << b
325
+ end
326
+ i += 1
327
+ end
328
+ out
329
+ end
330
+
331
+ def self.append_utf8(code, out)
332
+ if code <= 0x7F
333
+ out << code
334
+ elsif code <= 0x7FF
335
+ out << (0xC0 | (code >> 6)) << (0x80 | (code & 0x3F))
336
+ elsif code <= 0xFFFF
337
+ out << (0xE0 | (code >> 12)) << (0x80 | ((code >> 6) & 0x3F)) << (0x80 | (code & 0x3F))
338
+ elsif code <= 0x10FFFF
339
+ out << (0xF0 | (code >> 18)) << (0x80 | ((code >> 12) & 0x3F)) <<
340
+ (0x80 | ((code >> 6) & 0x3F)) << (0x80 | (code & 0x3F))
341
+ else
342
+ out << format("\\U%08x", code)
343
+ end
344
+ end
345
+
346
+ # @return [Boolean] whether `text` is a valid identifier
347
+ def self.identifier?(text)
348
+ text.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
349
+ end
350
+
351
+ private
352
+
353
+ def add_error(message)
354
+ @errors.call(@line, @column, message)
355
+ end
356
+
357
+ def next_char
358
+ if @char == 10
359
+ @line += 1
360
+ @column = 0
361
+ elsif @char == 9
362
+ @column += TAB_WIDTH - @column % TAB_WIDTH
363
+ else
364
+ @column += 1
365
+ end
366
+ @pos += 1
367
+ if @pos < @size
368
+ @char = @src.getbyte(@pos)
369
+ else
370
+ @read_error = true
371
+ @char = 0
372
+ end
373
+ end
374
+
375
+ def record_to(target)
376
+ @record_target = target
377
+ @record_start = @pos
378
+ end
379
+
380
+ def stop_recording
381
+ if @pos != @record_start
382
+ @record_target << @src.byteslice(@record_start, [@pos, @size].min - @record_start)
383
+ end
384
+ @record_target = nil
385
+ @record_start = -1
386
+ end
387
+
388
+ def start_token
389
+ @current.type = :start
390
+ @current.text = +""
391
+ @current.line = @line
392
+ @current.column = @column
393
+ record_to(@current.text)
394
+ end
395
+
396
+ def end_token
397
+ stop_recording
398
+ @current.end_column = @column
399
+ end
400
+
401
+ def try_consume_one(table)
402
+ return false unless table[@char]
403
+ next_char
404
+ true
405
+ end
406
+
407
+ def try_consume(byte)
408
+ return false unless @char == byte
409
+ next_char
410
+ true
411
+ end
412
+
413
+ def consume_zero_or_more(table)
414
+ next_char while table[@char]
415
+ end
416
+
417
+ def consume_one_or_more(table, error)
418
+ if table[@char]
419
+ next_char while table[@char]
420
+ else
421
+ add_error(error)
422
+ end
423
+ end
424
+
425
+ # Consumes up to `count` hex digits, stopping at the first that is missing
426
+ def consume_hex(count)
427
+ count.times { return false unless try_consume_one(HEX) }
428
+ true
429
+ end
430
+
431
+ def consume_string(delimiter)
432
+ loop do
433
+ case @char
434
+ when 0
435
+ add_error("Unexpected end of string.")
436
+ return
437
+ when 10
438
+ add_error("Multiline strings are not allowed. Did you miss a \"?.")
439
+ return
440
+ when 92 # backslash
441
+ next_char
442
+ if try_consume_one(ESCAPE)
443
+ elsif try_consume_one(OCTAL)
444
+ elsif try_consume(120) || try_consume(88) # x X
445
+ add_error("Expected hex digits for escape sequence.") unless try_consume_one(HEX)
446
+ elsif try_consume(117) # u
447
+ add_error("Expected four hex digits for \\u escape sequence.") unless consume_hex(4)
448
+ elsif try_consume(85) # U
449
+ valid = 2.times.all? { try_consume(48) } && (try_consume(48) || try_consume(49)) && consume_hex(5)
450
+ add_error("Expected eight hex digits up to 10ffff for \\U escape sequence") unless valid
451
+ else
452
+ add_error("Invalid escape sequence in string literal.")
453
+ end
454
+ else
455
+ if @char == delimiter
456
+ next_char
457
+ return
458
+ end
459
+ next_char
460
+ end
461
+ end
462
+ end
463
+
464
+ def consume_number(started_with_zero, started_with_dot)
465
+ is_float = false
466
+ if started_with_zero && (try_consume(120) || try_consume(88)) # x X
467
+ consume_one_or_more(HEX, "\"0x\" must be followed by hex digits.")
468
+ elsif started_with_zero && DIGIT[@char]
469
+ consume_zero_or_more(OCTAL)
470
+ if DIGIT[@char]
471
+ add_error("Numbers starting with leading zero must be in octal.")
472
+ consume_zero_or_more(DIGIT)
473
+ end
474
+ else
475
+ if started_with_dot
476
+ is_float = true
477
+ consume_zero_or_more(DIGIT)
478
+ else
479
+ consume_zero_or_more(DIGIT)
480
+ if try_consume(46) # .
481
+ is_float = true
482
+ consume_zero_or_more(DIGIT)
483
+ end
484
+ end
485
+ if try_consume(101) || try_consume(69) # e E
486
+ is_float = true
487
+ try_consume(45) || try_consume(43) # - +
488
+ consume_one_or_more(DIGIT, "\"e\" must be followed by exponent.")
489
+ end
490
+ end
491
+
492
+ if LETTER[@char]
493
+ add_error("Need space between number and identifier.")
494
+ elsif @char == 46 # .
495
+ if is_float
496
+ add_error("Already saw decimal point or exponent; can't have another one.")
497
+ else
498
+ add_error("Hex and octal numbers must be integers.")
499
+ end
500
+ end
501
+ is_float ? :float : :integer
502
+ end
503
+
504
+ def consume_symbol
505
+ @errors.call(@line, @column, "Interpreting non ascii codepoint #{@char}.") if @char >= 0x80
506
+ next_char
507
+ end
508
+
509
+ def consume_line_comment(content)
510
+ record_to(content) if content
511
+ next_char while @char != 0 && @char != 10
512
+ try_consume(10)
513
+ stop_recording if content
514
+ end
515
+
516
+ def consume_block_comment(content)
517
+ start_line = @line
518
+ start_column = @column - 2
519
+ record_to(content) if content
520
+ loop do
521
+ next_char while @char != 0 && @char != 42 && @char != 47 && @char != 10 # * / \n
522
+ if try_consume(10)
523
+ stop_recording if content
524
+ consume_zero_or_more(WHITESPACE_NO_NEWLINE)
525
+ if try_consume(42) && try_consume(47) # * /
526
+ break
527
+ end
528
+ record_to(content) if content
529
+ elsif try_consume(42) && try_consume(47)
530
+ if content
531
+ stop_recording
532
+ content.replace(content.byteslice(0, content.bytesize - 2))
533
+ end
534
+ break
535
+ elsif try_consume(47) && @char == 42
536
+ add_error("\"/*\" inside block comment. Block comments cannot be nested.")
537
+ elsif @char == 0
538
+ add_error("End-of-file inside block comment.")
539
+ @errors.call(start_line, start_column, " Comment started here.")
540
+ stop_recording if content
541
+ break
542
+ end
543
+ end
544
+ end
545
+
546
+ def try_consume_comment_start
547
+ return nil unless try_consume(47) # /
548
+ if try_consume(47)
549
+ :line
550
+ elsif try_consume(42) # *
551
+ :block
552
+ else
553
+ @current.type = :symbol
554
+ @current.text = +"/"
555
+ @current.line = @line
556
+ @current.column = @column - 1
557
+ @current.end_column = @column
558
+ :slash
559
+ end
560
+ end
561
+
562
+ def try_consume_whitespace
563
+ if @report_newlines
564
+ if try_consume_one(WHITESPACE_NO_NEWLINE)
565
+ consume_zero_or_more(WHITESPACE_NO_NEWLINE)
566
+ @current.type = :whitespace
567
+ return true
568
+ end
569
+ return false
570
+ end
571
+ if try_consume_one(WHITESPACE)
572
+ consume_zero_or_more(WHITESPACE)
573
+ @current.type = :whitespace
574
+ return @report_whitespace
575
+ end
576
+ false
577
+ end
578
+
579
+ def try_consume_newline
580
+ return false unless @report_whitespace && @report_newlines
581
+ if try_consume(10)
582
+ @current.type = :newline
583
+ return true
584
+ end
585
+ false
586
+ end
587
+
588
+ # tokenizer.cc's CommentCollector
589
+ class CommentCollector
590
+ def initialize(prev_trailing, detached, next_leading)
591
+ @prev_trailing = prev_trailing
592
+ @detached = detached
593
+ @next_leading = next_leading
594
+ prev_trailing&.clear
595
+ detached&.clear
596
+ next_leading&.clear
597
+ @buffer = "".b
598
+ @num_comments = 0
599
+ @has_trailing_comment = false
600
+ @has_comment = false
601
+ @is_line_comment = false
602
+ @can_attach_to_prev = true
603
+ end
604
+
605
+ def buffer_for_line_comment
606
+ flush if @has_comment && !@is_line_comment
607
+ @has_comment = true
608
+ @is_line_comment = true
609
+ @buffer
610
+ end
611
+
612
+ def buffer_for_block_comment
613
+ flush if @has_comment
614
+ @has_comment = true
615
+ @is_line_comment = false
616
+ @buffer
617
+ end
618
+
619
+ def clear_buffer
620
+ @buffer = "".b
621
+ @has_comment = false
622
+ end
623
+
624
+ def flush
625
+ return unless @has_comment
626
+ if @can_attach_to_prev
627
+ @prev_trailing&.<<(@buffer)
628
+ @has_trailing_comment = true
629
+ @can_attach_to_prev = false
630
+ else
631
+ @detached&.push(@buffer)
632
+ end
633
+ clear_buffer
634
+ @num_comments += 1
635
+ end
636
+
637
+ def detach_from_prev
638
+ @can_attach_to_prev = false
639
+ end
640
+
641
+ def maybe_detach_comment
642
+ count = @num_comments
643
+ count += 1 if @has_comment
644
+ return unless count == 1
645
+ if @has_trailing_comment && @prev_trailing
646
+ @detached&.unshift(@prev_trailing.dup)
647
+ @prev_trailing.clear
648
+ end
649
+ flush
650
+ end
651
+
652
+ # The destructor: whatever is still buffered leads the next token
653
+ def finish
654
+ @next_leading&.replace(@buffer) if @next_leading && @has_comment
655
+ end
656
+ end
657
+ end
658
+ end
659
+ end