File: variable_usages_are_allowed.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (159 lines) | stat: -rw-r--r-- 5,597 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# frozen_string_literal: true
module GraphQL
  module StaticValidation
    module VariableUsagesAreAllowed
      def initialize(*)
        super
        # holds { name => ast_node } pairs
        @declared_variables = {}
      end

      def on_operation_definition(node, parent)
        @declared_variables = node.variables.each_with_object({}) { |var, memo| memo[var.name] = var }
        super
      end

      def on_argument(node, parent)
        node_values = if node.value.is_a?(Array)
          node.value
        else
          [node.value]
        end
        node_values = node_values.select { |value| value.is_a? GraphQL::Language::Nodes::VariableIdentifier }

        if node_values.any?
          argument_owner = case parent
          when GraphQL::Language::Nodes::Field
            context.field_definition
          when GraphQL::Language::Nodes::Directive
            context.directive_definition
          when GraphQL::Language::Nodes::InputObject
            arg_type = context.argument_definition.type.unwrap
            if arg_type.kind.input_object?
              arg_type
            else
              # This is some kind of error
              nil
            end
          else
            raise("Unexpected argument parent: #{parent}")
          end

          node_values.each do |node_value|
            var_defn_ast = @declared_variables[node_value.name]
            # Might be undefined :(
            # VariablesAreUsedAndDefined can't finalize its search until the end of the document.
            var_defn_ast && argument_owner && validate_usage(argument_owner, node, var_defn_ast)
          end
        end
        super
      end

      private

      def validate_usage(argument_owner, arg_node, ast_var)
        var_type = context.schema.type_from_ast(ast_var.type, context: context)
        if var_type.nil?
          return
        end
        if !ast_var.default_value.nil?
          unless var_type.kind.non_null?
            # If the value is required, but the argument is not,
            # and yet there's a non-nil default, then we impliclty
            # make the argument also a required type.
            var_type = var_type.to_non_null_type
          end
        end

        arg_defn = context.warden.get_argument(argument_owner, arg_node.name)
        arg_defn_type = arg_defn.type

        # If the argument is non-null, but it was given a default value,
        # then treat it as nullable in practice, see https://github.com/rmosolgo/graphql-ruby/issues/3793
        if arg_defn_type.non_null? && arg_defn.default_value?
          arg_defn_type = arg_defn_type.of_type
        end

        var_inner_type = var_type.unwrap
        arg_inner_type = arg_defn_type.unwrap

        var_type = wrap_var_type_with_depth_of_arg(var_type, arg_node)

        if var_inner_type != arg_inner_type
          create_error("Type mismatch", var_type, ast_var, arg_defn, arg_node)
        elsif list_dimension(var_type) != list_dimension(arg_defn_type)
          create_error("List dimension mismatch", var_type, ast_var, arg_defn, arg_node)
        elsif !non_null_levels_match(arg_defn_type, var_type)
          create_error("Nullability mismatch", var_type, ast_var, arg_defn, arg_node)
        end
      end

      def create_error(error_message, var_type, ast_var, arg_defn, arg_node)
        add_error(GraphQL::StaticValidation::VariableUsagesAreAllowedError.new(
          "#{error_message} on variable $#{ast_var.name} and argument #{arg_node.name} (#{var_type.to_type_signature} / #{arg_defn.type.to_type_signature})",
          nodes: arg_node,
          name: ast_var.name,
          type: var_type.to_type_signature,
          argument: arg_node.name,
          error: error_message
        ))
      end

      def wrap_var_type_with_depth_of_arg(var_type, arg_node)
        arg_node_value = arg_node.value
        return var_type unless arg_node_value.is_a?(Array)
        new_var_type = var_type

        depth_of_array(arg_node_value).times do
          # Since the array _is_ present, treat it like a non-null type
          # (It satisfies a non-null requirement AND a nullable requirement)
          new_var_type = new_var_type.to_list_type.to_non_null_type
        end

        new_var_type
      end

      # @return [Integer] Returns the max depth of `array`, or `0` if it isn't an array at all
      def depth_of_array(array)
        case array
        when Array
          max_child_depth = 0
          array.each do |item|
            item_depth = depth_of_array(item)
            if item_depth > max_child_depth
              max_child_depth = item_depth
            end
          end
          1 + max_child_depth
        else
          0
        end
      end

      def list_dimension(type)
        if type.kind.list?
          1 + list_dimension(type.of_type)
        elsif type.kind.non_null?
          list_dimension(type.of_type)
        else
          0
        end
      end

      def non_null_levels_match(arg_type, var_type)
        if arg_type.kind.non_null? && !var_type.kind.non_null?
          false
        elsif arg_type.kind.wraps? && var_type.kind.wraps?
          # If var_type is a non-null wrapper for a type, and arg_type is nullable, peel off the wrapper
          # That way, a var_type of `[DairyAnimal]!` works with an arg_type of `[DairyAnimal]`
          if var_type.kind.non_null? && !arg_type.kind.non_null?
            var_type = var_type.of_type
          end
          non_null_levels_match(arg_type.of_type, var_type.of_type)
        else
          true
        end
      end
    end
  end
end