# frozen_string_literal: true require "strscan" require "graphql/language/nodes" module GraphQL module Language class Parser include GraphQL::Language::Nodes include EmptyObjects class << self attr_accessor :cache def parse(graphql_str, filename: nil, trace: Tracing::NullTrace) self.new(graphql_str, filename: filename, trace: trace).parse end def parse_file(filename, trace: Tracing::NullTrace) if cache cache.fetch(filename) do parse(File.read(filename), filename: filename, trace: trace) end else parse(File.read(filename), filename: filename, trace: trace) end end end def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace) if graphql_str.nil? raise GraphQL::ParseError.new("No query string was present", nil, nil, nil) end @lexer = Lexer.new(graphql_str, filename: filename) @graphql_str = graphql_str @filename = filename @trace = trace end def parse @document ||= begin @trace.parse(query_string: @graphql_str) do document end rescue SystemStackError raise GraphQL::ParseError.new("This query is too large to execute.", nil, nil, @query_str, filename: @filename) end end private attr_reader :token_name def advance_token @token_name = @lexer.advance end def pos @lexer.pos end def document any_tokens = advance_token if !any_tokens # Only ignored characters is not a valid document raise GraphQL::ParseError.new("Unexpected end of document", nil, nil, @graphql_str) end defns = [] while !@lexer.eos? defns << definition end Document.new(pos: 0, definitions: defns, filename: @filename, source_string: @graphql_str) end def definition case token_name when :FRAGMENT loc = pos expect_token :FRAGMENT f_name = if !at?(:ON) parse_name end expect_token :ON f_type = parse_type_name directives = parse_directives selections = selection_set Nodes::FragmentDefinition.new( pos: loc, name: f_name, type: f_type, directives: directives, selections: selections, filename: @filename, source_string: @graphql_str ) when :QUERY, :MUTATION, :SUBSCRIPTION, :LCURLY op_loc = pos op_type = case token_name when :LCURLY "query" else parse_operation_type end op_name = at?(:IDENTIFIER) ? parse_name : nil variable_definitions = if at?(:LPAREN) expect_token(:LPAREN) defs = [] while !at?(:RPAREN) loc = pos expect_token(:VAR_SIGN) var_name = parse_name expect_token(:COLON) var_type = self.type default_value = if at?(:EQUALS) advance_token value end defs << Nodes::VariableDefinition.new(pos: loc, name: var_name, type: var_type, default_value: default_value, filename: @filename, source_string: @graphql_str) end expect_token(:RPAREN) defs else EmptyObjects::EMPTY_ARRAY end directives = parse_directives OperationDefinition.new( pos: op_loc, operation_type: op_type, name: op_name, variables: variable_definitions, directives: directives, selections: selection_set, filename: @filename, source_string: @graphql_str ) when :EXTEND loc = pos advance_token case token_name when :SCALAR advance_token name = parse_name directives = parse_directives ScalarTypeExtension.new(pos: loc, name: name, directives: directives, filename: @filename, source_string: @graphql_str) when :TYPE advance_token name = parse_name implements_interfaces = parse_implements directives = parse_directives field_defns = at?(:LCURLY) ? parse_field_definitions : EMPTY_ARRAY ObjectTypeExtension.new(pos: loc, name: name, interfaces: implements_interfaces, directives: directives, fields: field_defns, filename: @filename, source_string: @graphql_str) when :INTERFACE advance_token name = parse_name directives = parse_directives interfaces = parse_implements fields_definition = at?(:LCURLY) ? parse_field_definitions : EMPTY_ARRAY InterfaceTypeExtension.new(pos: loc, name: name, directives: directives, fields: fields_definition, interfaces: interfaces, filename: @filename, source_string: @graphql_str) when :UNION advance_token name = parse_name directives = parse_directives union_member_types = parse_union_members UnionTypeExtension.new(pos: loc, name: name, directives: directives, types: union_member_types, filename: @filename, source_string: @graphql_str) when :ENUM advance_token name = parse_name directives = parse_directives enum_values_definition = parse_enum_value_definitions Nodes::EnumTypeExtension.new(pos: loc, name: name, directives: directives, values: enum_values_definition, filename: @filename, source_string: @graphql_str) when :INPUT advance_token name = parse_name directives = parse_directives input_fields_definition = parse_input_object_field_definitions InputObjectTypeExtension.new(pos: loc, name: name, directives: directives, fields: input_fields_definition, filename: @filename, source_string: @graphql_str) when :SCHEMA advance_token directives = parse_directives query = mutation = subscription = nil if at?(:LCURLY) advance_token while !at?(:RCURLY) if at?(:QUERY) advance_token expect_token(:COLON) query = parse_name elsif at?(:MUTATION) advance_token expect_token(:COLON) mutation = parse_name elsif at?(:SUBSCRIPTION) advance_token expect_token(:COLON) subscription = parse_name else expect_one_of([:QUERY, :MUTATION, :SUBSCRIPTION]) end end expect_token :RCURLY end SchemaExtension.new( subscription: subscription, mutation: mutation, query: query, directives: directives, pos: loc, filename: @filename, source_string: @graphql_str, ) else expect_one_of([:SCHEMA, :SCALAR, :TYPE, :ENUM, :INPUT, :UNION, :INTERFACE]) end else loc = pos desc = at?(:STRING) ? string_value : nil defn_loc = pos case token_name when :SCHEMA advance_token directives = parse_directives query = mutation = subscription = nil expect_token :LCURLY while !at?(:RCURLY) if at?(:QUERY) advance_token expect_token(:COLON) query = parse_name elsif at?(:MUTATION) advance_token expect_token(:COLON) mutation = parse_name elsif at?(:SUBSCRIPTION) advance_token expect_token(:COLON) subscription = parse_name else expect_one_of([:QUERY, :MUTATION, :SUBSCRIPTION]) end end expect_token :RCURLY SchemaDefinition.new(pos: loc, definition_pos: defn_loc, query: query, mutation: mutation, subscription: subscription, directives: directives, filename: @filename, source_string: @graphql_str) when :DIRECTIVE advance_token expect_token :DIR_SIGN name = parse_name arguments_definition = parse_argument_definitions repeatable = if at?(:REPEATABLE) advance_token true else false end expect_token :ON directive_locations = [DirectiveLocation.new(pos: pos, name: parse_name, filename: @filename, source_string: @graphql_str)] while at?(:PIPE) advance_token directive_locations << DirectiveLocation.new(pos: pos, name: parse_name, filename: @filename, source_string: @graphql_str) end DirectiveDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, arguments: arguments_definition, locations: directive_locations, repeatable: repeatable, filename: @filename, source_string: @graphql_str) when :TYPE advance_token name = parse_name implements_interfaces = parse_implements directives = parse_directives field_defns = at?(:LCURLY) ? parse_field_definitions : EMPTY_ARRAY ObjectTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, interfaces: implements_interfaces, directives: directives, fields: field_defns, filename: @filename, source_string: @graphql_str) when :INTERFACE advance_token name = parse_name interfaces = parse_implements directives = parse_directives fields_definition = parse_field_definitions InterfaceTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, fields: fields_definition, interfaces: interfaces, filename: @filename, source_string: @graphql_str) when :UNION advance_token name = parse_name directives = parse_directives union_member_types = parse_union_members UnionTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, types: union_member_types, filename: @filename, source_string: @graphql_str) when :SCALAR advance_token name = parse_name directives = parse_directives ScalarTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, filename: @filename, source_string: @graphql_str) when :ENUM advance_token name = parse_name directives = parse_directives enum_values_definition = parse_enum_value_definitions Nodes::EnumTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, values: enum_values_definition, filename: @filename, source_string: @graphql_str) when :INPUT advance_token name = parse_name directives = parse_directives input_fields_definition = parse_input_object_field_definitions InputObjectTypeDefinition.new(pos: loc, definition_pos: defn_loc, description: desc, name: name, directives: directives, fields: input_fields_definition, filename: @filename, source_string: @graphql_str) else expect_one_of([:SCHEMA, :SCALAR, :TYPE, :ENUM, :INPUT, :UNION, :INTERFACE]) end end end def parse_input_object_field_definitions if at?(:LCURLY) expect_token :LCURLY list = [] while !at?(:RCURLY) list << parse_input_value_definition end expect_token :RCURLY list else EMPTY_ARRAY end end def parse_enum_value_definitions if at?(:LCURLY) expect_token :LCURLY list = [] while !at?(:RCURLY) v_loc = pos description = if at?(:STRING); string_value; end defn_loc = pos enum_value = expect_token_value(:IDENTIFIER) v_directives = parse_directives list << EnumValueDefinition.new(pos: v_loc, definition_pos: defn_loc, description: description, name: enum_value, directives: v_directives, filename: @filename, source_string: @graphql_str) end expect_token :RCURLY list else EMPTY_ARRAY end end def parse_union_members if at?(:EQUALS) expect_token :EQUALS list = [parse_type_name] while at?(:PIPE) advance_token list << parse_type_name end list else EMPTY_ARRAY end end def parse_implements if at?(:IMPLEMENTS) advance_token list = [] while true advance_token if at?(:AMP) break unless at?(:IDENTIFIER) list << parse_type_name end list else EMPTY_ARRAY end end def parse_field_definitions expect_token :LCURLY list = [] while !at?(:RCURLY) loc = pos description = if at?(:STRING); string_value; end defn_loc = pos name = parse_name arguments_definition = parse_argument_definitions expect_token :COLON type = self.type directives = parse_directives list << FieldDefinition.new(pos: loc, definition_pos: defn_loc, description: description, name: name, arguments: arguments_definition, type: type, directives: directives, filename: @filename, source_string: @graphql_str) end expect_token :RCURLY list end def parse_argument_definitions if at?(:LPAREN) advance_token list = [] while !at?(:RPAREN) list << parse_input_value_definition end expect_token :RPAREN list else EMPTY_ARRAY end end def parse_input_value_definition loc = pos description = if at?(:STRING); string_value; end defn_loc = pos name = parse_name expect_token :COLON type = self.type default_value = if at?(:EQUALS) advance_token value else nil end directives = parse_directives InputValueDefinition.new(pos: loc, definition_pos: defn_loc, description: description, name: name, type: type, default_value: default_value, directives: directives, filename: @filename, source_string: @graphql_str) end def type type = case token_name when :IDENTIFIER parse_type_name when :LBRACKET list_type end if at?(:BANG) type = Nodes::NonNullType.new(pos: pos, of_type: type) expect_token(:BANG) end type end def list_type loc = pos expect_token(:LBRACKET) type = Nodes::ListType.new(pos: loc, of_type: self.type) expect_token(:RBRACKET) type end def parse_operation_type val = if at?(:QUERY) "query" elsif at?(:MUTATION) "mutation" elsif at?(:SUBSCRIPTION) "subscription" else expect_one_of([:QUERY, :MUTATION, :SUBSCRIPTION]) end advance_token val end def selection_set expect_token(:LCURLY) selections = [] while @token_name != :RCURLY selections << if at?(:ELLIPSIS) loc = pos advance_token case token_name when :ON, :DIR_SIGN, :LCURLY if_type = if at?(:ON) advance_token parse_type_name else nil end directives = parse_directives Nodes::InlineFragment.new(pos: loc, type: if_type, directives: directives, selections: selection_set, filename: @filename, source_string: @graphql_str) else name = parse_name_without_on directives = parse_directives # Can this ever happen? # expect_token(:IDENTIFIER) if at?(:ON) FragmentSpread.new(pos: loc, name: name, directives: directives, filename: @filename, source_string: @graphql_str) end else loc = pos name = parse_name field_alias = nil if at?(:COLON) advance_token field_alias = name name = parse_name end arguments = at?(:LPAREN) ? parse_arguments : nil directives = at?(:DIR_SIGN) ? parse_directives : nil selection_set = at?(:LCURLY) ? self.selection_set : nil Nodes::Field.new(pos: loc, field_alias: field_alias, name: name, arguments: arguments, directives: directives, selections: selection_set, filename: @filename, source_string: @graphql_str) end end expect_token(:RCURLY) selections end def parse_name case token_name when :IDENTIFIER expect_token_value(:IDENTIFIER) when :SCHEMA advance_token "schema" when :SCALAR advance_token "scalar" when :IMPLEMENTS advance_token "implements" when :INTERFACE advance_token "interface" when :UNION advance_token "union" when :ENUM advance_token "enum" when :INPUT advance_token "input" when :DIRECTIVE advance_token "directive" when :TYPE advance_token "type" when :QUERY advance_token "query" when :MUTATION advance_token "mutation" when :SUBSCRIPTION advance_token "subscription" when :TRUE advance_token "true" when :FALSE advance_token "false" when :FRAGMENT advance_token "fragment" when :REPEATABLE advance_token "repeatable" when :NULL advance_token "null" else expect_token(:NAME) end end def parse_name_without_on if at?(:ON) expect_token(:IDENTIFIER) else parse_name end end # Any identifier, but not true, false, or null def parse_enum_name if at?(:TRUE) || at?(:FALSE) || at?(:NULL) expect_token(:IDENTIFIER) else parse_name end end def parse_type_name TypeName.new(pos: pos, name: parse_name, filename: @filename, source_string: @graphql_str) end def parse_directives if at?(:DIR_SIGN) dirs = [] while at?(:DIR_SIGN) loc = pos advance_token name = parse_name arguments = parse_arguments dirs << Nodes::Directive.new(pos: loc, name: name, arguments: arguments, filename: @filename, source_string: @graphql_str) end dirs else EMPTY_ARRAY end end def parse_arguments if at?(:LPAREN) advance_token args = [] while !at?(:RPAREN) loc = pos name = parse_name expect_token(:COLON) args << Nodes::Argument.new(pos: loc, name: name, value: value, filename: @filename, source_string: @graphql_str) end if args.empty? expect_token(:ARGUMENT_NAME) # At least one argument is required end expect_token(:RPAREN) args else EMPTY_ARRAY end end def string_value token_value = @lexer.string_value expect_token :STRING token_value end def value case token_name when :INT expect_token_value(:INT).to_i when :FLOAT expect_token_value(:FLOAT).to_f when :STRING string_value when :TRUE advance_token true when :FALSE advance_token false when :NULL advance_token NullValue.new(pos: pos, name: "null", filename: @filename, source_string: @graphql_str) when :IDENTIFIER Nodes::Enum.new(pos: pos, name: expect_token_value(:IDENTIFIER), filename: @filename, source_string: @graphql_str) when :LBRACKET advance_token list = [] while !at?(:RBRACKET) list << value end expect_token(:RBRACKET) list when :LCURLY start = pos advance_token args = [] while !at?(:RCURLY) loc = pos n = parse_name expect_token(:COLON) args << Argument.new(pos: loc, name: n, value: value, filename: @filename, source_string: @graphql_str) end expect_token(:RCURLY) InputObject.new(pos: start, arguments: args, filename: @filename, source_string: @graphql_str) when :VAR_SIGN loc = pos advance_token VariableIdentifier.new(pos: loc, name: parse_name, filename: @filename, source_string: @graphql_str) else expect_token(:VALUE) end end def at?(expected_token_name) @token_name == expected_token_name end def expect_token(expected_token_name) unless @token_name == expected_token_name raise_parse_error("Expected #{expected_token_name}, actual: #{token_name || "(none)"} (#{debug_token_value.inspect})") end advance_token end def expect_one_of(token_names) raise_parse_error("Expected one of #{token_names.join(", ")}, actual: #{token_name || "NOTHING"} (#{debug_token_value.inspect})") end def raise_parse_error(message) message += " at [#{@lexer.line_number}, #{@lexer.column_number}]" raise GraphQL::ParseError.new( message, @lexer.line_number, @lexer.column_number, @graphql_str, filename: @filename, ) end # Only use when we care about the expected token's value def expect_token_value(tok) token_value = @lexer.token_value expect_token(tok) token_value end # token_value works for when the scanner matched something # which is usually fine and it's good for it to be fast at that. def debug_token_value if token_name && Lexer::Punctuation.const_defined?(token_name) Lexer::Punctuation.const_get(token_name) elsif token_name == :ELLIPSIS "..." else @lexer.token_value end end end end end