Skip to content

Commit d3d22ef

Browse files
committed
Add path to validation errors
1 parent 0a38007 commit d3d22ef

38 files changed

+434
-315
lines changed

lib/graphql/language/parser.rb

Lines changed: 195 additions & 195 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/graphql/static_validation/message.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,27 @@ class Message
66
# Convenience for validators
77
module MessageHelper
88
# Error `message` is located at `node`
9-
def message(message, node)
10-
GraphQL::StaticValidation::Message.new(message, line: node.line, col: node.col)
9+
def message(message, node, context: nil, path: nil)
10+
path ||= context.path
11+
GraphQL::StaticValidation::Message.new(message, line: node.line, col: node.col, path: path)
1112
end
1213
end
13-
attr_reader :message, :line, :col
1414

15-
def initialize(message, line: nil, col: nil)
15+
attr_reader :message, :line, :col, :path
16+
17+
def initialize(message, line: nil, col: nil, path: [])
1618
@message = message
1719
@line = line
1820
@col = col
21+
@path = path
1922
end
2023

2124
# A hash representation of this Message
2225
def to_h
2326
{
2427
"message" => message,
25-
"locations" => locations
28+
"locations" => locations,
29+
"path" => path,
2630
}
2731
end
2832

lib/graphql/static_validation/rules/argument_literals_are_compatible.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def validate_node(parent, node, defn, context)
1010
if !valid
1111
kind_of_node = node_type(parent)
1212
error_arg_name = parent_name(parent, defn)
13-
context.errors << message("Argument '#{node.name}' on #{kind_of_node} '#{error_arg_name}' has an invalid value", parent)
13+
context.errors << message("Argument '#{node.name}' on #{kind_of_node} '#{error_arg_name}' has an invalid value", parent, context: context)
1414
end
1515
end
1616
end

lib/graphql/static_validation/rules/arguments_are_defined.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def validate_node(parent, node, defn, context)
66
if argument_defn.nil?
77
kind_of_node = node_type(parent)
88
error_arg_name = parent_name(parent, defn)
9-
context.errors << message("#{kind_of_node} '#{error_arg_name}' doesn't accept argument '#{node.name}'", parent)
9+
context.errors << message("#{kind_of_node} '#{error_arg_name}' doesn't accept argument '#{node.name}'", parent, context: context)
1010
GraphQL::Language::Visitor::SKIP
1111
else
1212
nil

lib/graphql/static_validation/rules/directives_are_defined.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ class DirectivesAreDefined
66
def validate(context)
77
directive_names = context.schema.directives.keys
88
context.visitor[GraphQL::Language::Nodes::Directive] << -> (node, parent) {
9-
validate_directive(node, directive_names, context.errors)
9+
validate_directive(node, directive_names, context)
1010
}
1111
end
1212

1313
private
1414

15-
def validate_directive(ast_directive, directive_names, errors)
15+
def validate_directive(ast_directive, directive_names, context)
1616
if !directive_names.include?(ast_directive.name)
17-
errors << message("Directive @#{ast_directive.name} is not defined", ast_directive)
17+
context.errors << message("Directive @#{ast_directive.name} is not defined", ast_directive, context: context)
1818
end
1919
end
2020
end

lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def validate(context)
88
directives = context.schema.directives
99

1010
context.visitor[Nodes::Directive] << -> (node, parent) {
11-
validate_location(node, parent, directives, context.errors)
11+
validate_location(node, parent, directives, context)
1212
}
1313
end
1414

@@ -33,25 +33,25 @@ def validate(context)
3333

3434
SIMPLE_LOCATION_NODES = SIMPLE_LOCATIONS.keys
3535

36-
def validate_location(ast_directive, ast_parent, directives, errors)
36+
def validate_location(ast_directive, ast_parent, directives, context)
3737
directive_defn = directives[ast_directive.name]
3838
case ast_parent
3939
when Nodes::OperationDefinition
4040
required_location = GraphQL::Directive.const_get(ast_parent.operation_type.upcase)
41-
assert_includes_location(directive_defn, ast_directive, required_location, errors)
41+
assert_includes_location(directive_defn, ast_directive, required_location, context)
4242
when *SIMPLE_LOCATION_NODES
4343
required_location = SIMPLE_LOCATIONS[ast_parent.class]
44-
assert_includes_location(directive_defn, ast_directive, required_location, errors)
44+
assert_includes_location(directive_defn, ast_directive, required_location, context)
4545
else
46-
errors << message("Directives can't be applied to #{ast_parent.class.name}s", ast_directive)
46+
context.errors << message("Directives can't be applied to #{ast_parent.class.name}s", ast_directive, context: context)
4747
end
4848
end
4949

50-
def assert_includes_location(directive_defn, directive_ast, required_location, errors)
50+
def assert_includes_location(directive_defn, directive_ast, required_location, context)
5151
if !directive_defn.locations.include?(required_location)
5252
location_name = LOCATION_MESSAGE_NAMES[required_location]
5353
allowed_location_names = directive_defn.locations.map { |loc| LOCATION_MESSAGE_NAMES[loc] }
54-
errors << message("'@#{directive_defn.name}' can't be applied to #{location_name} (allowed: #{allowed_location_names.join(", ")})", directive_ast)
54+
context.errors << message("'@#{directive_defn.name}' can't be applied to #{location_name} (allowed: #{allowed_location_names.join(", ")})", directive_ast, context: context)
5555
end
5656
end
5757
end

lib/graphql/static_validation/rules/fields_are_defined_on_type.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,21 @@ def validate(context)
99
return if context.skip_field?(node.name)
1010
parent_type = context.object_types[-2]
1111
parent_type = parent_type.unwrap
12-
validate_field(context.errors, node, parent_type, parent)
12+
validate_field(context, node, parent_type, parent)
1313
}
1414
end
1515

1616
private
1717

18-
def validate_field(errors, ast_field, parent_type, parent)
18+
def validate_field(context, ast_field, parent_type, parent)
1919
if parent_type.kind.union?
20-
errors << message("Selections can't be made directly on unions (see selections on #{parent_type.name})", parent)
20+
context.errors << message("Selections can't be made directly on unions (see selections on #{parent_type.name})", parent, context: context)
2121
return GraphQL::Language::Visitor::SKIP
2222
end
2323

2424
field = parent_type.get_field(ast_field.name)
2525
if field.nil?
26-
errors << message("Field '#{ast_field.name}' doesn't exist on type '#{parent_type.name}'", parent)
26+
context.errors << message("Field '#{ast_field.name}' doesn't exist on type '#{parent_type.name}'", parent, context: context)
2727
return GraphQL::Language::Visitor::SKIP
2828
end
2929
end

lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,25 @@ def validate(context)
99
context.visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) {
1010
return if context.skip_field?(node.name)
1111
field_defn = context.field_definition
12-
validate_field_selections(node, field_defn, context.errors)
12+
validate_field_selections(node, field_defn, context)
1313
}
1414
end
1515

1616
private
1717

18-
def validate_field_selections(ast_field, field_defn, errors)
18+
def validate_field_selections(ast_field, field_defn, context)
1919
resolved_type = field_defn.type.unwrap
2020

2121
if resolved_type.kind.scalar? && ast_field.selections.any?
22-
error = message("Selections can't be made on scalars (field '#{ast_field.name}' returns #{resolved_type.name} but has selections [#{ast_field.selections.map(&:name).join(", ")}])", ast_field)
22+
error = message("Selections can't be made on scalars (field '#{ast_field.name}' returns #{resolved_type.name} but has selections [#{ast_field.selections.map(&:name).join(", ")}])", ast_field, context: context)
2323
elsif resolved_type.kind.object? && ast_field.selections.none?
24-
error = message("Objects must have selections (field '#{ast_field.name}' returns #{resolved_type.name} but has no selections)", ast_field)
24+
error = message("Objects must have selections (field '#{ast_field.name}' returns #{resolved_type.name} but has no selections)", ast_field, context: context)
2525
else
2626
error = nil
2727
end
2828

2929
if !error.nil?
30-
errors << error
30+
context.errors << error
3131
GraphQL::Language::Visitor::SKIP
3232
end
3333
end

lib/graphql/static_validation/rules/fields_will_merge.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def validate(context)
2222

2323
def find_conflicts(field_map, context)
2424
field_map.each do |name, ast_fields|
25-
comparison = FieldDefinitionComparison.new(name, ast_fields)
25+
comparison = FieldDefinitionComparison.new(name, ast_fields, context)
2626
context.errors.push(*comparison.errors)
2727

2828

@@ -63,27 +63,27 @@ class FieldDefinitionComparison
6363
include GraphQL::StaticValidation::Message::MessageHelper
6464
NAMED_VALUES = [GraphQL::Language::Nodes::Enum, GraphQL::Language::Nodes::VariableIdentifier]
6565
attr_reader :errors
66-
def initialize(name, defs)
66+
def initialize(name, defs, context)
6767
errors = []
6868

6969
names = defs.map(&:name).uniq
7070
if names.length != 1
71-
errors << message("Field '#{name}' has a field conflict: #{names.join(" or ")}?", defs.first)
71+
errors << message("Field '#{name}' has a field conflict: #{names.join(" or ")}?", defs.first, context: context)
7272
end
7373

7474
args = defs.map { |defn| reduce_list(defn.arguments)}.uniq
7575
if args.length != 1
76-
errors << message("Field '#{name}' has an argument conflict: #{args.map {|a| JSON.dump(a) }.join(" or ")}?", defs.first)
76+
errors << message("Field '#{name}' has an argument conflict: #{args.map {|a| JSON.dump(a) }.join(" or ")}?", defs.first, context: context)
7777
end
7878

7979
directive_names = defs.map { |defn| defn.directives.map(&:name) }.uniq
8080
if directive_names.length != 1
81-
errors << message("Field '#{name}' has a directive conflict: #{directive_names.map {|names| "[#{names.join(", ")}]"}.join(" or ")}?", defs.first)
81+
errors << message("Field '#{name}' has a directive conflict: #{directive_names.map {|names| "[#{names.join(", ")}]"}.join(" or ")}?", defs.first, context: context)
8282
end
8383

8484
directive_args = defs.map {|defn| defn.directives.map {|d| reduce_list(d.arguments) } }.uniq
8585
if directive_args.length != 1
86-
errors << message("Field '#{name}' has a directive argument conflict: #{directive_args.map {|args| JSON.dump(args)}.join(" or ")}?", defs.first)
86+
errors << message("Field '#{name}' has a directive argument conflict: #{directive_args.map {|args| JSON.dump(args)}.join(" or ")}?", defs.first, context: context)
8787
end
8888

8989
@errors = errors

lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,33 @@ def validate(context)
99
fragment_parent = context.object_types[-2]
1010
fragment_child = context.object_types.last
1111
if fragment_child
12-
validate_fragment_in_scope(fragment_parent, fragment_child, node, context)
12+
validate_fragment_in_scope(fragment_parent, fragment_child, node, context, context.path)
1313
end
1414
}
1515

1616
spreads_to_validate = []
1717

1818
context.visitor[GraphQL::Language::Nodes::FragmentSpread] << -> (node, parent) {
1919
fragment_parent = context.object_types.last
20-
spreads_to_validate << [node, fragment_parent]
20+
spreads_to_validate << FragmentSpread.new(node: node, parent_type: fragment_parent, path: context.path)
2121
}
2222

23-
context.visitor[GraphQL::Language::Nodes::Document].leave << -> (node, parent) {
24-
spreads_to_validate.each do |spread_values|
25-
node, fragment_parent = spread_values
26-
fragment_child_name = context.fragments[node.name].type
23+
context.visitor[GraphQL::Language::Nodes::Document].leave << -> (doc_node, parent) {
24+
spreads_to_validate.each do |frag_spread|
25+
fragment_child_name = context.fragments[frag_spread.node.name].type
2726
fragment_child = context.schema.types[fragment_child_name]
28-
validate_fragment_in_scope(fragment_parent, fragment_child, node, context)
27+
validate_fragment_in_scope(frag_spread.parent_type, fragment_child, frag_spread.node, context, frag_spread.path)
2928
end
3029
}
3130
end
3231

3332
private
3433

35-
def validate_fragment_in_scope(parent_type, child_type, node, context)
34+
def validate_fragment_in_scope(parent_type, child_type, node, context, path)
3635
intersecting_types = get_possible_types(parent_type, context.schema) & get_possible_types(child_type, context.schema)
3736
if intersecting_types.none?
3837
name = node.respond_to?(:name) ? " #{node.name}" : ""
39-
context.errors << message("Fragment#{name} on #{child_type.name} can't be spread inside #{parent_type.name}", node)
38+
context.errors << message("Fragment#{name} on #{child_type.name} can't be spread inside #{parent_type.name}", node, path: path)
4039
end
4140
end
4241

@@ -51,6 +50,15 @@ def get_possible_types(type, schema)
5150
[]
5251
end
5352
end
53+
54+
class FragmentSpread
55+
attr_reader :node, :parent_type, :path
56+
def initialize(node:, parent_type:, path:)
57+
@node = node
58+
@parent_type = parent_type
59+
@path = path
60+
end
61+
end
5462
end
5563
end
5664
end

0 commit comments

Comments
 (0)