graphql 2.0.30 → 2.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (157) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install/mutation_root_generator.rb +2 -2
  3. data/lib/generators/graphql/install/templates/base_mutation.erb +2 -0
  4. data/lib/generators/graphql/install/templates/mutation_type.erb +2 -0
  5. data/lib/generators/graphql/install_generator.rb +3 -0
  6. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  7. data/lib/generators/graphql/templates/base_connection.erb +2 -0
  8. data/lib/generators/graphql/templates/base_edge.erb +2 -0
  9. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  10. data/lib/generators/graphql/templates/base_field.erb +2 -0
  11. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  12. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  13. data/lib/generators/graphql/templates/base_object.erb +2 -0
  14. data/lib/generators/graphql/templates/base_resolver.erb +6 -0
  15. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  16. data/lib/generators/graphql/templates/base_union.erb +2 -0
  17. data/lib/generators/graphql/templates/graphql_controller.erb +2 -0
  18. data/lib/generators/graphql/templates/loader.erb +2 -0
  19. data/lib/generators/graphql/templates/mutation.erb +2 -0
  20. data/lib/generators/graphql/templates/node_type.erb +2 -0
  21. data/lib/generators/graphql/templates/query_type.erb +2 -0
  22. data/lib/generators/graphql/templates/schema.erb +5 -0
  23. data/lib/graphql/analysis/analyzer.rb +89 -0
  24. data/lib/graphql/analysis/field_usage.rb +82 -0
  25. data/lib/graphql/analysis/max_query_complexity.rb +20 -0
  26. data/lib/graphql/analysis/max_query_depth.rb +20 -0
  27. data/lib/graphql/analysis/query_complexity.rb +183 -0
  28. data/lib/graphql/analysis/query_depth.rb +58 -0
  29. data/lib/graphql/analysis/visitor.rb +282 -0
  30. data/lib/graphql/analysis.rb +92 -1
  31. data/lib/graphql/backtrace/inspect_result.rb +0 -12
  32. data/lib/graphql/backtrace/trace.rb +12 -15
  33. data/lib/graphql/coercion_error.rb +1 -9
  34. data/lib/graphql/dataloader/async_dataloader.rb +88 -0
  35. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  36. data/lib/graphql/dataloader/request.rb +5 -0
  37. data/lib/graphql/dataloader/source.rb +11 -3
  38. data/lib/graphql/dataloader.rb +112 -142
  39. data/lib/graphql/duration_encoding_error.rb +16 -0
  40. data/lib/graphql/execution/interpreter/argument_value.rb +5 -1
  41. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +175 -0
  42. data/lib/graphql/execution/interpreter/runtime.rb +163 -365
  43. data/lib/graphql/execution/interpreter.rb +92 -158
  44. data/lib/graphql/execution/lookahead.rb +88 -21
  45. data/lib/graphql/introspection/dynamic_fields.rb +1 -1
  46. data/lib/graphql/introspection/entry_points.rb +11 -5
  47. data/lib/graphql/introspection/schema_type.rb +3 -1
  48. data/lib/graphql/language/block_string.rb +34 -18
  49. data/lib/graphql/language/definition_slice.rb +1 -1
  50. data/lib/graphql/language/document_from_schema_definition.rb +38 -38
  51. data/lib/graphql/language/lexer.rb +305 -193
  52. data/lib/graphql/language/nodes.rb +113 -66
  53. data/lib/graphql/language/parser.rb +787 -1986
  54. data/lib/graphql/language/printer.rb +303 -146
  55. data/lib/graphql/language/sanitized_printer.rb +20 -22
  56. data/lib/graphql/language/static_visitor.rb +167 -0
  57. data/lib/graphql/language/visitor.rb +20 -81
  58. data/lib/graphql/language.rb +61 -0
  59. data/lib/graphql/load_application_object_failed_error.rb +5 -1
  60. data/lib/graphql/pagination/array_connection.rb +6 -6
  61. data/lib/graphql/pagination/connection.rb +28 -1
  62. data/lib/graphql/pagination/mongoid_relation_connection.rb +1 -2
  63. data/lib/graphql/query/context/scoped_context.rb +101 -0
  64. data/lib/graphql/query/context.rb +66 -131
  65. data/lib/graphql/query/null_context.rb +4 -11
  66. data/lib/graphql/query/validation_pipeline.rb +4 -4
  67. data/lib/graphql/query/variables.rb +3 -3
  68. data/lib/graphql/query.rb +17 -26
  69. data/lib/graphql/railtie.rb +9 -6
  70. data/lib/graphql/rake_task.rb +3 -12
  71. data/lib/graphql/rubocop/graphql/base_cop.rb +1 -1
  72. data/lib/graphql/schema/addition.rb +21 -11
  73. data/lib/graphql/schema/argument.rb +43 -8
  74. data/lib/graphql/schema/base_64_encoder.rb +3 -5
  75. data/lib/graphql/schema/build_from_definition.rb +9 -12
  76. data/lib/graphql/schema/directive/one_of.rb +12 -0
  77. data/lib/graphql/schema/directive/specified_by.rb +14 -0
  78. data/lib/graphql/schema/directive.rb +3 -1
  79. data/lib/graphql/schema/enum.rb +3 -3
  80. data/lib/graphql/schema/field/connection_extension.rb +1 -15
  81. data/lib/graphql/schema/field/scope_extension.rb +8 -1
  82. data/lib/graphql/schema/field.rb +49 -35
  83. data/lib/graphql/schema/has_single_input_argument.rb +157 -0
  84. data/lib/graphql/schema/input_object.rb +4 -4
  85. data/lib/graphql/schema/interface.rb +10 -10
  86. data/lib/graphql/schema/introspection_system.rb +4 -2
  87. data/lib/graphql/schema/late_bound_type.rb +4 -0
  88. data/lib/graphql/schema/list.rb +2 -2
  89. data/lib/graphql/schema/loader.rb +2 -3
  90. data/lib/graphql/schema/member/base_dsl_methods.rb +2 -1
  91. data/lib/graphql/schema/member/has_arguments.rb +63 -73
  92. data/lib/graphql/schema/member/has_directives.rb +1 -1
  93. data/lib/graphql/schema/member/has_fields.rb +8 -5
  94. data/lib/graphql/schema/member/has_interfaces.rb +23 -9
  95. data/lib/graphql/schema/member/relay_shortcuts.rb +1 -1
  96. data/lib/graphql/schema/member/scoped.rb +19 -0
  97. data/lib/graphql/schema/member/type_system_helpers.rb +1 -2
  98. data/lib/graphql/schema/member/validates_input.rb +3 -3
  99. data/lib/graphql/schema/mutation.rb +7 -0
  100. data/lib/graphql/schema/object.rb +8 -0
  101. data/lib/graphql/schema/printer.rb +8 -7
  102. data/lib/graphql/schema/relay_classic_mutation.rb +6 -128
  103. data/lib/graphql/schema/resolver.rb +27 -13
  104. data/lib/graphql/schema/scalar.rb +3 -3
  105. data/lib/graphql/schema/subscription.rb +11 -4
  106. data/lib/graphql/schema/union.rb +1 -1
  107. data/lib/graphql/schema/unique_within_type.rb +1 -1
  108. data/lib/graphql/schema/warden.rb +96 -95
  109. data/lib/graphql/schema.rb +323 -102
  110. data/lib/graphql/static_validation/all_rules.rb +1 -1
  111. data/lib/graphql/static_validation/base_visitor.rb +1 -1
  112. data/lib/graphql/static_validation/literal_validator.rb +2 -3
  113. data/lib/graphql/static_validation/rules/fields_will_merge.rb +2 -2
  114. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  115. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +2 -2
  116. data/lib/graphql/static_validation/validation_context.rb +5 -5
  117. data/lib/graphql/static_validation/validator.rb +3 -0
  118. data/lib/graphql/static_validation.rb +0 -1
  119. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +4 -3
  120. data/lib/graphql/subscriptions/broadcast_analyzer.rb +1 -1
  121. data/lib/graphql/subscriptions/event.rb +8 -2
  122. data/lib/graphql/subscriptions/serialize.rb +2 -0
  123. data/lib/graphql/subscriptions.rb +15 -13
  124. data/lib/graphql/testing/helpers.rb +151 -0
  125. data/lib/graphql/testing.rb +2 -0
  126. data/lib/graphql/tracing/appoptics_trace.rb +2 -2
  127. data/lib/graphql/tracing/appoptics_tracing.rb +2 -2
  128. data/lib/graphql/tracing/legacy_hooks_trace.rb +74 -0
  129. data/lib/graphql/tracing/platform_tracing.rb +3 -1
  130. data/lib/graphql/tracing/{prometheus_tracing → prometheus_trace}/graphql_collector.rb +3 -1
  131. data/lib/graphql/tracing/prometheus_trace.rb +9 -9
  132. data/lib/graphql/tracing/sentry_trace.rb +112 -0
  133. data/lib/graphql/tracing/trace.rb +1 -0
  134. data/lib/graphql/tracing.rb +3 -1
  135. data/lib/graphql/type_kinds.rb +1 -1
  136. data/lib/graphql/types/iso_8601_duration.rb +77 -0
  137. data/lib/graphql/types/relay/connection_behaviors.rb +32 -2
  138. data/lib/graphql/types/relay/edge_behaviors.rb +7 -0
  139. data/lib/graphql/types.rb +1 -0
  140. data/lib/graphql/version.rb +1 -1
  141. data/lib/graphql.rb +13 -13
  142. data/readme.md +12 -2
  143. metadata +33 -26
  144. data/lib/graphql/analysis/ast/analyzer.rb +0 -84
  145. data/lib/graphql/analysis/ast/field_usage.rb +0 -57
  146. data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
  147. data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
  148. data/lib/graphql/analysis/ast/query_complexity.rb +0 -230
  149. data/lib/graphql/analysis/ast/query_depth.rb +0 -55
  150. data/lib/graphql/analysis/ast/visitor.rb +0 -276
  151. data/lib/graphql/analysis/ast.rb +0 -81
  152. data/lib/graphql/deprecation.rb +0 -9
  153. data/lib/graphql/filter.rb +0 -59
  154. data/lib/graphql/language/parser.y +0 -560
  155. data/lib/graphql/schema/base_64_bp.rb +0 -26
  156. data/lib/graphql/static_validation/type_stack.rb +0 -216
  157. data/lib/graphql/subscriptions/instrumentation.rb +0 -28
@@ -1,39 +1,246 @@
1
1
  # frozen_string_literal: true
2
-
3
- require "strscan"
4
-
5
2
  module GraphQL
6
3
  module Language
4
+
7
5
  class Lexer
8
- IDENTIFIER = /[_A-Za-z][_0-9A-Za-z]*/
9
- NEWLINE = /[\c\r\n]/
10
- BLANK = /[, \t]+/
11
- COMMENT = /#[^\n\r]*/
12
- INT = /[-]?(?:[0]|[1-9][0-9]*)/
13
- FLOAT_DECIMAL = /[.][0-9]+/
14
- FLOAT_EXP = /[eE][+-]?[0-9]+/
15
- FLOAT = /#{INT}(#{FLOAT_DECIMAL}#{FLOAT_EXP}|#{FLOAT_DECIMAL}|#{FLOAT_EXP})/
16
-
17
- module Literals
18
- ON = /on\b/
19
- FRAGMENT = /fragment\b/
20
- TRUE = /true\b/
21
- FALSE = /false\b/
22
- NULL = /null\b/
23
- QUERY = /query\b/
24
- MUTATION = /mutation\b/
25
- SUBSCRIPTION = /subscription\b/
26
- SCHEMA = /schema\b/
27
- SCALAR = /scalar\b/
28
- TYPE = /type\b/
29
- EXTEND = /extend\b/
30
- IMPLEMENTS = /implements\b/
31
- INTERFACE = /interface\b/
32
- UNION = /union\b/
33
- ENUM = /enum\b/
34
- INPUT = /input\b/
35
- DIRECTIVE = /directive\b/
36
- REPEATABLE = /repeatable\b/
6
+ def initialize(graphql_str, filename: nil, max_tokens: nil)
7
+ if !(graphql_str.encoding == Encoding::UTF_8 || graphql_str.ascii_only?)
8
+ graphql_str = graphql_str.dup.force_encoding(Encoding::UTF_8)
9
+ end
10
+ @string = graphql_str
11
+ @filename = filename
12
+ @scanner = StringScanner.new(graphql_str)
13
+ @pos = nil
14
+ @max_tokens = max_tokens || Float::INFINITY
15
+ @tokens_count = 0
16
+ end
17
+
18
+ def eos?
19
+ @scanner.eos?
20
+ end
21
+
22
+ attr_reader :pos
23
+
24
+ def advance
25
+ @scanner.skip(IGNORE_REGEXP)
26
+ return false if @scanner.eos?
27
+ @tokens_count += 1
28
+ if @tokens_count > @max_tokens
29
+ raise_parse_error("This query is too large to execute.")
30
+ end
31
+ @pos = @scanner.pos
32
+ next_byte = @string.getbyte(@pos)
33
+ next_byte_is_for = FIRST_BYTES[next_byte]
34
+ case next_byte_is_for
35
+ when ByteFor::PUNCTUATION
36
+ @scanner.pos += 1
37
+ PUNCTUATION_NAME_FOR_BYTE[next_byte]
38
+ when ByteFor::NAME
39
+ if len = @scanner.skip(KEYWORD_REGEXP)
40
+ case len
41
+ when 2
42
+ :ON
43
+ when 12
44
+ :SUBSCRIPTION
45
+ else
46
+ pos = @pos
47
+
48
+ # Use bytes 2 and 3 as a unique identifier for this keyword
49
+ bytes = (@string.getbyte(pos + 2) << 8) | @string.getbyte(pos + 1)
50
+ KEYWORD_BY_TWO_BYTES[_hash(bytes)]
51
+ end
52
+ else
53
+ @scanner.skip(IDENTIFIER_REGEXP)
54
+ :IDENTIFIER
55
+ end
56
+ when ByteFor::IDENTIFIER
57
+ @scanner.skip(IDENTIFIER_REGEXP)
58
+ :IDENTIFIER
59
+ when ByteFor::NUMBER
60
+ @scanner.skip(NUMERIC_REGEXP)
61
+
62
+ if GraphQL.reject_numbers_followed_by_names
63
+ new_pos = @scanner.pos
64
+ peek_byte = @string.getbyte(new_pos)
65
+ next_first_byte = FIRST_BYTES[peek_byte]
66
+ if next_first_byte == ByteFor::NAME || next_first_byte == ByteFor::IDENTIFIER
67
+ number_part = token_value
68
+ name_part = @scanner.scan(IDENTIFIER_REGEXP)
69
+ raise_parse_error("Name after number is not allowed (in `#{number_part}#{name_part}`)")
70
+ end
71
+ end
72
+ # Check for a matched decimal:
73
+ @scanner[1] ? :FLOAT : :INT
74
+ when ByteFor::ELLIPSIS
75
+ if @string.getbyte(@pos + 1) != 46 || @string.getbyte(@pos + 2) != 46
76
+ raise_parse_error("Expected `...`, actual: #{@string[@pos..@pos + 2].inspect}")
77
+ end
78
+ @scanner.pos += 3
79
+ :ELLIPSIS
80
+ when ByteFor::STRING
81
+ if @scanner.skip(BLOCK_STRING_REGEXP) || @scanner.skip(QUOTED_STRING_REGEXP)
82
+ :STRING
83
+ else
84
+ raise_parse_error("Expected string or block string, but it was malformed")
85
+ end
86
+ else
87
+ @scanner.pos += 1
88
+ :UNKNOWN_CHAR
89
+ end
90
+ rescue ArgumentError => err
91
+ if err.message == "invalid byte sequence in UTF-8"
92
+ raise_parse_error("Parse error on bad Unicode escape sequence", nil, nil)
93
+ end
94
+ end
95
+
96
+ def token_value
97
+ @string.byteslice(@scanner.pos - @scanner.matched_size, @scanner.matched_size)
98
+ rescue StandardError => err
99
+ raise GraphQL::Error, "(token_value failed: #{err.class}: #{err.message})"
100
+ end
101
+
102
+ def debug_token_value(token_name)
103
+ if token_name && Lexer::Punctuation.const_defined?(token_name)
104
+ Lexer::Punctuation.const_get(token_name)
105
+ elsif token_name == :ELLIPSIS
106
+ "..."
107
+ elsif token_name == :STRING
108
+ string_value
109
+ elsif @scanner.matched_size.nil?
110
+ @scanner.peek(1)
111
+ else
112
+ token_value
113
+ end
114
+ end
115
+
116
+ ESCAPES = /\\["\\\/bfnrt]/
117
+ ESCAPES_REPLACE = {
118
+ '\\"' => '"',
119
+ "\\\\" => "\\",
120
+ "\\/" => '/',
121
+ "\\b" => "\b",
122
+ "\\f" => "\f",
123
+ "\\n" => "\n",
124
+ "\\r" => "\r",
125
+ "\\t" => "\t",
126
+ }
127
+ UTF_8 = /\\u(?:([\dAa-f]{4})|\{([\da-f]{4,})\})(?:\\u([\dAa-f]{4}))?/i
128
+ VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o
129
+ ESCAPED = /(?:#{ESCAPES}|#{UTF_8})/o
130
+
131
+ def string_value
132
+ str = token_value
133
+ is_block = str.start_with?('"""')
134
+ if is_block
135
+ str.gsub!(/\A"""|"""\z/, '')
136
+ return Language::BlockString.trim_whitespace(str)
137
+ else
138
+ str.gsub!(/\A"|"\z/, '')
139
+
140
+ if !str.valid_encoding? || !str.match?(VALID_STRING)
141
+ raise_parse_error("Bad unicode escape in #{str.inspect}")
142
+ else
143
+ Lexer.replace_escaped_characters_in_place(str)
144
+
145
+ if !str.valid_encoding?
146
+ raise_parse_error("Bad unicode escape in #{str.inspect}")
147
+ else
148
+ str
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ def line_number
155
+ @scanner.string[0..@pos].count("\n") + 1
156
+ end
157
+
158
+ def column_number
159
+ @scanner.string[0..@pos].split("\n").last.length
160
+ end
161
+
162
+ def raise_parse_error(message, line = line_number, col = column_number)
163
+ raise GraphQL::ParseError.new(message, line, col, @string, filename: @filename)
164
+ end
165
+
166
+ IGNORE_REGEXP = %r{
167
+ (?:
168
+ [, \c\r\n\t]+ |
169
+ \#.*$
170
+ )*
171
+ }x
172
+ IDENTIFIER_REGEXP = /[_A-Za-z][_0-9A-Za-z]*/
173
+ INT_REGEXP = /-?(?:[0]|[1-9][0-9]*)/
174
+ FLOAT_DECIMAL_REGEXP = /[.][0-9]+/
175
+ FLOAT_EXP_REGEXP = /[eE][+-]?[0-9]+/
176
+ # TODO: FLOAT_EXP_REGEXP should not be allowed to follow INT_REGEXP, integers are not allowed to have exponent parts.
177
+ NUMERIC_REGEXP = /#{INT_REGEXP}(#{FLOAT_DECIMAL_REGEXP}#{FLOAT_EXP_REGEXP}|#{FLOAT_DECIMAL_REGEXP}|#{FLOAT_EXP_REGEXP})?/
178
+
179
+ KEYWORDS = [
180
+ "on",
181
+ "fragment",
182
+ "true",
183
+ "false",
184
+ "null",
185
+ "query",
186
+ "mutation",
187
+ "subscription",
188
+ "schema",
189
+ "scalar",
190
+ "type",
191
+ "extend",
192
+ "implements",
193
+ "interface",
194
+ "union",
195
+ "enum",
196
+ "input",
197
+ "directive",
198
+ "repeatable"
199
+ ].freeze
200
+
201
+ KEYWORD_REGEXP = /#{Regexp.union(KEYWORDS.sort)}\b/
202
+ KEYWORD_BY_TWO_BYTES = [
203
+ :INTERFACE,
204
+ :MUTATION,
205
+ :EXTEND,
206
+ :FALSE,
207
+ :ENUM,
208
+ :TRUE,
209
+ :NULL,
210
+ nil,
211
+ nil,
212
+ nil,
213
+ nil,
214
+ nil,
215
+ nil,
216
+ nil,
217
+ :QUERY,
218
+ nil,
219
+ nil,
220
+ :REPEATABLE,
221
+ :IMPLEMENTS,
222
+ :INPUT,
223
+ :TYPE,
224
+ :SCHEMA,
225
+ nil,
226
+ nil,
227
+ nil,
228
+ :DIRECTIVE,
229
+ :UNION,
230
+ nil,
231
+ nil,
232
+ :SCALAR,
233
+ nil,
234
+ :FRAGMENT
235
+ ]
236
+
237
+ # This produces a unique integer for bytes 2 and 3 of each keyword string
238
+ # See https://tenderlovemaking.com/2023/09/02/fast-tokenizers-with-stringscanner.html
239
+ def _hash key
240
+ (key * 18592990) >> 27 & 0x1f
241
+ end
242
+
243
+ module Punctuation
37
244
  LCURLY = '{'
38
245
  RCURLY = '}'
39
246
  LPAREN = '('
@@ -43,36 +250,30 @@ module GraphQL
43
250
  COLON = ':'
44
251
  VAR_SIGN = '$'
45
252
  DIR_SIGN = '@'
46
- ELLIPSIS = '...'
47
253
  EQUALS = '='
48
254
  BANG = '!'
49
255
  PIPE = '|'
50
256
  AMP = '&'
51
257
  end
52
258
 
53
- include Literals
259
+ # A sparse array mapping the bytes for each punctuation
260
+ # to a symbol name for that punctuation
261
+ PUNCTUATION_NAME_FOR_BYTE = Punctuation.constants.each_with_object([]) { |name, arr|
262
+ punct = Punctuation.const_get(name)
263
+ arr[punct.ord] = name
264
+ }
54
265
 
55
266
  QUOTE = '"'
56
267
  UNICODE_DIGIT = /[0-9A-Za-z]/
57
268
  FOUR_DIGIT_UNICODE = /#{UNICODE_DIGIT}{4}/
58
- N_DIGIT_UNICODE = %r{#{LCURLY}#{UNICODE_DIGIT}{4,}#{RCURLY}}x
269
+ N_DIGIT_UNICODE = %r{#{Punctuation::LCURLY}#{UNICODE_DIGIT}{4,}#{Punctuation::RCURLY}}x
59
270
  UNICODE_ESCAPE = %r{\\u(?:#{FOUR_DIGIT_UNICODE}|#{N_DIGIT_UNICODE})}
60
- # # https://graphql.github.io/graphql-spec/June2018/#sec-String-Value
61
271
  STRING_ESCAPE = %r{[\\][\\/bfnrt]}
62
272
  BLOCK_QUOTE = '"""'
63
273
  ESCAPED_QUOTE = /\\"/;
64
- STRING_CHAR = /#{ESCAPED_QUOTE}|[^"\\]|#{UNICODE_ESCAPE}|#{STRING_ESCAPE}/
65
-
66
- LIT_NAME_LUT = Literals.constants.each_with_object({}) { |n, o|
67
- key = Literals.const_get(n)
68
- key = key.is_a?(Regexp) ? key.source.gsub(/(\\b|\\)/, '') : key
69
- o[key] = n
70
- }
71
-
72
- LIT = Regexp.union(Literals.constants.map { |n| Literals.const_get(n) })
73
-
74
- QUOTED_STRING = %r{#{QUOTE} (?:#{STRING_CHAR})* #{QUOTE}}x
75
- BLOCK_STRING = %r{
274
+ STRING_CHAR = /#{ESCAPED_QUOTE}|[^"\\\n\r]|#{UNICODE_ESCAPE}|#{STRING_ESCAPE}/
275
+ QUOTED_STRING_REGEXP = %r{#{QUOTE} (?:#{STRING_CHAR})* #{QUOTE}}x
276
+ BLOCK_STRING_REGEXP = %r{
76
277
  #{BLOCK_QUOTE}
77
278
  (?: [^"\\] | # Any characters that aren't a quote or slash
78
279
  (?<!") ["]{1,2} (?!") | # Any quotes that don't have quotes next to them
@@ -84,169 +285,80 @@ module GraphQL
84
285
  #{BLOCK_QUOTE}
85
286
  }xm
86
287
 
87
- # # catch-all for anything else. must be at the bottom for precedence.
88
- UNKNOWN_CHAR = /./
89
-
90
- def initialize(value)
91
- @line = 1
92
- @col = 1
93
- @previous_token = nil
94
-
95
- @scan = scanner value
288
+ # Use this array to check, for a given byte that will start a token,
289
+ # what kind of token might it start?
290
+ FIRST_BYTES = Array.new(255)
291
+
292
+ module ByteFor
293
+ NUMBER = 0 # int or float
294
+ NAME = 1 # identifier or keyword
295
+ STRING = 2
296
+ ELLIPSIS = 3
297
+ IDENTIFIER = 4 # identifier, *not* a keyword
298
+ PUNCTUATION = 5
96
299
  end
97
300
 
98
- class BadEncoding < Lexer # :nodoc:
99
- def scanner(value)
100
- [emit(:BAD_UNICODE_ESCAPE, 0, 0, value)]
101
- end
102
-
103
- def next_token
104
- @scan.pop
105
- end
301
+ (0..9).each { |i| FIRST_BYTES[i.to_s.ord] = ByteFor::NUMBER }
302
+ FIRST_BYTES["-".ord] = ByteFor::NUMBER
303
+ # Some of these may be overwritten below, if keywords start with the same character
304
+ ("A".."Z").each { |char| FIRST_BYTES[char.ord] = ByteFor::IDENTIFIER }
305
+ ("a".."z").each { |char| FIRST_BYTES[char.ord] = ByteFor::IDENTIFIER }
306
+ FIRST_BYTES['_'.ord] = ByteFor::IDENTIFIER
307
+ FIRST_BYTES['.'.ord] = ByteFor::ELLIPSIS
308
+ FIRST_BYTES['"'.ord] = ByteFor::STRING
309
+ KEYWORDS.each { |kw| FIRST_BYTES[kw.getbyte(0)] = ByteFor::NAME }
310
+ Punctuation.constants.each do |punct_name|
311
+ punct = Punctuation.const_get(punct_name)
312
+ FIRST_BYTES[punct.ord] = ByteFor::PUNCTUATION
106
313
  end
107
314
 
108
- def self.tokenize(string)
109
- value = string.dup.force_encoding(Encoding::UTF_8)
110
-
111
- scanner = if value.valid_encoding?
112
- new value
113
- else
114
- BadEncoding.new value
115
- end
116
-
117
- toks = []
118
-
119
- while tok = scanner.next_token
120
- toks << tok
121
- end
122
-
123
- toks
124
- end
125
-
126
- def next_token
127
- return if @scan.eos?
128
-
129
- pos = @scan.pos
130
-
131
- case
132
- when str = @scan.scan(FLOAT) then emit(:FLOAT, pos, @scan.pos, str)
133
- when str = @scan.scan(INT) then emit(:INT, pos, @scan.pos, str)
134
- when str = @scan.scan(LIT) then emit(LIT_NAME_LUT[str], pos, @scan.pos, -str)
135
- when str = @scan.scan(IDENTIFIER) then emit(:IDENTIFIER, pos, @scan.pos, str)
136
- when str = @scan.scan(BLOCK_STRING) then emit_block(pos, @scan.pos, str.gsub(/\A#{BLOCK_QUOTE}|#{BLOCK_QUOTE}\z/, ''))
137
- when str = @scan.scan(QUOTED_STRING) then emit_string(pos, @scan.pos, str.gsub(/^"|"$/, ''))
138
- when str = @scan.scan(COMMENT) then record_comment(pos, @scan.pos, str)
139
- when str = @scan.scan(NEWLINE)
140
- @line += 1
141
- @col = 1
142
- next_token
143
- when @scan.scan(BLANK)
144
- @col += @scan.pos - pos
145
- next_token
146
- when str = @scan.scan(UNKNOWN_CHAR) then emit(:UNKNOWN_CHAR, pos, @scan.pos, str)
147
- else
148
- # This should never happen since `UNKNOWN_CHAR` ensures we make progress
149
- raise "Unknown string?"
150
- end
151
- end
152
-
153
- def emit(token_name, ts, te, token_value)
154
- token = [
155
- token_name,
156
- @line,
157
- @col,
158
- token_value,
159
- @previous_token,
160
- ]
161
- @previous_token = token
162
- # Bump the column counter for the next token
163
- @col += te - ts
164
- token
165
- end
166
315
 
167
316
  # Replace any escaped unicode or whitespace with the _actual_ characters
168
317
  # To avoid allocating more strings, this modifies the string passed into it
169
318
  def self.replace_escaped_characters_in_place(raw_string)
170
- raw_string.gsub!(ESCAPES, ESCAPES_REPLACE)
171
- raw_string.gsub!(UTF_8) do |_matched_str|
172
- codepoint_1 = ($1 || $2).to_i(16)
173
- codepoint_2 = $3
174
-
175
- if codepoint_2
176
- codepoint_2 = codepoint_2.to_i(16)
177
- if (codepoint_1 >= 0xD800 && codepoint_1 <= 0xDBFF) && # leading surrogate
178
- (codepoint_2 >= 0xDC00 && codepoint_2 <= 0xDFFF) # trailing surrogate
179
- # A surrogate pair
180
- combined = ((codepoint_1 - 0xD800) * 0x400) + (codepoint_2 - 0xDC00) + 0x10000
181
- [combined].pack('U'.freeze)
319
+ raw_string.gsub!(ESCAPED) do |matched_str|
320
+ if (point_str_1 = $1 || $2)
321
+ codepoint_1 = point_str_1.to_i(16)
322
+ if (codepoint_2 = $3)
323
+ codepoint_2 = codepoint_2.to_i(16)
324
+ if (codepoint_1 >= 0xD800 && codepoint_1 <= 0xDBFF) && # leading surrogate
325
+ (codepoint_2 >= 0xDC00 && codepoint_2 <= 0xDFFF) # trailing surrogate
326
+ # A surrogate pair
327
+ combined = ((codepoint_1 - 0xD800) * 0x400) + (codepoint_2 - 0xDC00) + 0x10000
328
+ [combined].pack('U'.freeze)
329
+ else
330
+ # Two separate code points
331
+ [codepoint_1].pack('U'.freeze) + [codepoint_2].pack('U'.freeze)
332
+ end
182
333
  else
183
- # Two separate code points
184
- [codepoint_1].pack('U'.freeze) + [codepoint_2].pack('U'.freeze)
334
+ [codepoint_1].pack('U'.freeze)
185
335
  end
186
336
  else
187
- [codepoint_1].pack('U'.freeze)
337
+ ESCAPES_REPLACE[matched_str]
188
338
  end
189
339
  end
190
340
  nil
191
341
  end
192
342
 
193
- def record_comment(ts, te, str)
194
- token = [
195
- :COMMENT,
196
- @line,
197
- @col,
198
- str,
199
- @previous_token,
200
- ]
201
-
202
- @previous_token = token
203
-
204
- @col += te - ts
205
- next_token
206
- end
207
-
208
- ESCAPES = /\\["\\\/bfnrt]/
209
- ESCAPES_REPLACE = {
210
- '\\"' => '"',
211
- "\\\\" => "\\",
212
- "\\/" => '/',
213
- "\\b" => "\b",
214
- "\\f" => "\f",
215
- "\\n" => "\n",
216
- "\\r" => "\r",
217
- "\\t" => "\t",
218
- }
219
- UTF_8 = /\\u(?:([\dAa-f]{4})|\{([\da-f]{4,})\})(?:\\u([\dAa-f]{4}))?/i
220
- VALID_STRING = /\A(?:[^\\]|#{ESCAPES}|#{UTF_8})*\z/o
221
-
222
- def emit_block(ts, te, value)
223
- line_incr = value.count("\n")
224
- value = GraphQL::Language::BlockString.trim_whitespace(value)
225
- tok = emit_string(ts, te, value)
226
- @line += line_incr
227
- tok
228
- end
229
-
230
- def emit_string(ts, te, value)
231
- if !value.valid_encoding? || !value.match?(VALID_STRING)
232
- emit(:BAD_UNICODE_ESCAPE, ts, te, value)
233
- else
234
- self.class.replace_escaped_characters_in_place(value)
235
-
236
- if !value.valid_encoding?
237
- emit(:BAD_UNICODE_ESCAPE, ts, te, value)
238
- else
239
- emit(:STRING, ts, te, value)
240
- end
343
+ # This is not used during parsing because the parser
344
+ # doesn't actually need tokens.
345
+ def self.tokenize(string)
346
+ lexer = GraphQL::Language::Lexer.new(string)
347
+ tokens = []
348
+ prev_token = nil
349
+ while (token_name = lexer.advance)
350
+ new_token = [
351
+ token_name,
352
+ lexer.line_number,
353
+ lexer.column_number,
354
+ lexer.debug_token_value(token_name),
355
+ prev_token,
356
+ ]
357
+ tokens << new_token
358
+ prev_token = new_token
241
359
  end
360
+ tokens
242
361
  end
243
-
244
- private
245
-
246
- def scanner(value)
247
- StringScanner.new value
248
- end
249
-
250
362
  end
251
363
  end
252
364
  end