Skip to content

Commit 3c494fc

Browse files
gordyffacebook-github-bot
authored andcommitted
required-on-nonnull-field checking to consider fields in sibling inline fragments
Reviewed By: captbaritone Differential Revision: D77563673 fbshipit-source-id: ae23cc510427ecb28b287f94f07a0887714d70e5
1 parent 94f107a commit 3c494fc

File tree

4 files changed

+152
-14
lines changed

4 files changed

+152
-14
lines changed

compiler/crates/relay-transforms/src/validations/disallow_required_on_non_null_field.rs

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use std::collections::HashMap;
89
use std::sync::Arc;
910

1011
use ::errors::try_all;
1112
use common::Diagnostic;
1213
use common::DiagnosticTag;
1314
use common::DiagnosticsResult;
1415
use common::DirectiveName;
16+
use common::Location;
1517
use common::NamedItem;
1618
use errors::try2;
1719
use graphql_ir::Field;
@@ -20,6 +22,7 @@ use graphql_ir::Program;
2022
use graphql_ir::Selection;
2123
use graphql_ir::Validator;
2224
use graphql_ir::reexport::Intern;
25+
use intern::string_key::StringKey;
2326
use lazy_static::lazy_static;
2427
use schema::SDLSchema;
2528
use schema::Schema;
@@ -47,19 +50,53 @@ pub fn disallow_required_on_non_null_field(program: &Program) -> DiagnosticsResu
4750
}
4851
}
4952

53+
type FieldPath = Vec<StringKey>;
54+
5055
struct DisallowRequiredOnNonNullField<'a> {
5156
schema: &'a Arc<SDLSchema>,
5257
warnings: Vec<Diagnostic>,
58+
path: FieldPath,
59+
modifiable_fields: HashMap<FieldPath, Action>,
5360
}
5461

5562
impl<'a> DisallowRequiredOnNonNullField<'a> {
5663
fn new(schema: &'a Arc<SDLSchema>) -> Self {
5764
Self {
5865
schema,
5966
warnings: vec![],
67+
path: vec![],
68+
modifiable_fields: HashMap::new(),
6069
}
6170
}
6271

72+
// Tracks field required-directive removal eligibility. If a field's required directive,
73+
// is not removable, it remains not removable forever. Otherwise it is removable and we
74+
// accumulate the locations where it can be removed... unless it becomes not removable
75+
// later on.
76+
fn update_field_action(&mut self, new_action: Action) {
77+
let mut path = self.path.clone();
78+
path.reverse();
79+
let existing_action = self.modifiable_fields.get(&path);
80+
self.modifiable_fields.insert(
81+
path,
82+
match existing_action {
83+
None => new_action,
84+
Some(Action::NotRemovable) => {
85+
// already not removable, keep it that way
86+
Action::NotRemovable
87+
}
88+
Some(Action::Removable(list)) => match new_action {
89+
Action::NotRemovable => Action::NotRemovable,
90+
Action::Removable(new_list) => {
91+
let mut new_list = new_list.clone();
92+
new_list.extend(list.clone());
93+
Action::Removable(new_list)
94+
}
95+
},
96+
},
97+
);
98+
}
99+
63100
fn validate_required_field(
64101
&mut self,
65102
field: &Arc<impl Field>,
@@ -81,11 +118,10 @@ impl<'a> DisallowRequiredOnNonNullField<'a> {
81118
.type_
82119
.is_non_null()
83120
{
84-
self.warnings.push(Diagnostic::hint_with_data(
85-
ValidationMessageWithData::RequiredOnNonNull,
86-
required_directive.unwrap().location,
87-
vec![DiagnosticTag::UNNECESSARY],
88-
));
121+
self.update_field_action(Action::Removable(vec![Message {
122+
message: ValidationMessageWithData::RequiredOnNonNull,
123+
location: required_directive.unwrap().location,
124+
}]));
89125
} else if self
90126
.schema
91127
.field(field.definition().item)
@@ -94,11 +130,12 @@ impl<'a> DisallowRequiredOnNonNullField<'a> {
94130
.is_some()
95131
{
96132
// @required on a semantically-non-null field is unnecessary
97-
self.warnings.push(Diagnostic::hint_with_data(
98-
ValidationMessageWithData::RequiredOnSemanticNonNull,
99-
required_directive.unwrap().location,
100-
vec![DiagnosticTag::UNNECESSARY],
101-
));
133+
self.update_field_action(Action::Removable(vec![Message {
134+
message: ValidationMessageWithData::RequiredOnSemanticNonNull,
135+
location: required_directive.unwrap().location,
136+
}]));
137+
} else {
138+
self.update_field_action(Action::NotRemovable);
102139
}
103140

104141
Ok(())
@@ -125,22 +162,52 @@ impl<'a> DisallowRequiredOnNonNullField<'a> {
125162
None => self.validate_required_field(linked_field, errors_are_caught),
126163
};
127164

165+
self.path.push(linked_field.alias_or_name(self.schema));
166+
128167
let selection_result =
129168
self.validate_selection_fields(&linked_field.selections, errors_are_caught);
130169

131170
try2(field_result, selection_result)?;
171+
self.path.pop();
132172
Ok(())
133173
}
134174
Selection::ScalarField(scalar_field) => {
135175
self.validate_required_field(scalar_field, errors_are_caught)
136176
}
137177
Selection::InlineFragment(fragment) => {
138-
self.validate_selection_fields(&fragment.selections, errors_are_caught)
178+
if let Ok(Some(alias)) = fragment.alias(self.schema) {
179+
self.path.push(alias.item);
180+
let result =
181+
self.validate_selection_fields(&fragment.selections, errors_are_caught);
182+
self.path.pop();
183+
result
184+
} else {
185+
self.validate_selection_fields(&fragment.selections, errors_are_caught)
186+
}
139187
}
140188
_ => Ok(()),
141189
}))?;
142190
Ok(())
143191
}
192+
193+
fn modifiable_fields_to_warnings(&mut self) {
194+
for (path, action) in self.modifiable_fields.iter() {
195+
match action {
196+
Action::NotRemovable => {}
197+
Action::Removable(message_list) => {
198+
for message in message_list {
199+
self.warnings.push(Diagnostic::hint_with_data(
200+
message.message.clone(),
201+
message.location,
202+
vec![DiagnosticTag::UNNECESSARY],
203+
));
204+
}
205+
}
206+
}
207+
208+
println!("path: {:?}", path);
209+
}
210+
}
144211
}
145212

146213
impl Validator for DisallowRequiredOnNonNullField<'_> {
@@ -149,22 +216,41 @@ impl Validator for DisallowRequiredOnNonNullField<'_> {
149216
const VALIDATE_DIRECTIVES: bool = false;
150217

151218
fn validate_fragment(&mut self, fragment: &FragmentDefinition) -> DiagnosticsResult<()> {
219+
self.modifiable_fields.clear();
152220
let throw_on_field_error_directive =
153221
fragment.directives.named(*THROW_ON_FIELD_ERROR_DIRECTIVE);
154222

155223
let has_throw_on_field_error_directive = throw_on_field_error_directive.is_some();
156224

157-
self.validate_selection_fields(&fragment.selections, has_throw_on_field_error_directive)
225+
let ret = self
226+
.validate_selection_fields(&fragment.selections, has_throw_on_field_error_directive);
227+
self.modifiable_fields_to_warnings();
228+
ret
158229
}
159230

160231
fn validate_operation(
161232
&mut self,
162233
operation: &graphql_ir::OperationDefinition,
163234
) -> DiagnosticsResult<()> {
235+
self.modifiable_fields.clear();
164236
let throw_on_field_error_directive =
165237
operation.directives.named(*THROW_ON_FIELD_ERROR_DIRECTIVE);
166238

167239
let has_throw_on_field_error_directive = throw_on_field_error_directive.is_some();
168-
self.validate_selection_fields(&operation.selections, has_throw_on_field_error_directive)
240+
let result = self
241+
.validate_selection_fields(&operation.selections, has_throw_on_field_error_directive);
242+
self.modifiable_fields_to_warnings();
243+
result
169244
}
170245
}
246+
247+
#[derive(Clone, Debug)]
248+
struct Message {
249+
location: Location,
250+
message: ValidationMessageWithData,
251+
}
252+
253+
enum Action {
254+
NotRemovable,
255+
Removable(Vec<Message>),
256+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
==================================== INPUT ====================================
2+
fragment MyFragment on Animal @throwOnFieldError {
3+
... on Cat {
4+
noise @required(action: THROW) # can't be removed due to Dog's noise also being present
5+
}
6+
... on Dog {
7+
noise @required(action: THROW) # can't be removed
8+
}
9+
}
10+
11+
# %extensions%
12+
13+
interface Animal {
14+
name: String!
15+
}
16+
17+
type Cat implements Animal {
18+
noise: String!
19+
}
20+
type Dog implements Animal {
21+
noise: String # nullable
22+
}
23+
==================================== OUTPUT ===================================
24+
OK
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
fragment MyFragment on Animal @throwOnFieldError {
2+
... on Cat {
3+
noise @required(action: THROW) # can't be removed due to Dog's noise also being present
4+
}
5+
... on Dog {
6+
noise @required(action: THROW) # can't be removed
7+
}
8+
}
9+
10+
# %extensions%
11+
12+
interface Animal {
13+
name: String!
14+
}
15+
16+
type Cat implements Animal {
17+
noise: String!
18+
}
19+
type Dog implements Animal {
20+
noise: String # nullable
21+
}

compiler/crates/relay-transforms/tests/disallow_required_on_non_null_field_test.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<b7654cbc0db7924c3b85107ada9f49ba>>
7+
* @generated SignedSource<<a5fdf04a1c7ede81277ad2983d671375>>
88
*/
99

1010
mod disallow_required_on_non_null_field;
@@ -19,6 +19,13 @@ async fn fragment_with_inline_fragment_required_non_null_fields_invalid() {
1919
test_fixture(transform_fixture, file!(), "fragment_with_inline_fragment_required_non_null_fields.invalid.graphql", "disallow_required_on_non_null_field/fixtures/fragment_with_inline_fragment_required_non_null_fields.invalid.expected", input, expected).await;
2020
}
2121

22+
#[tokio::test]
23+
async fn fragment_with_multiple_inline_fragment_required_fields() {
24+
let input = include_str!("disallow_required_on_non_null_field/fixtures/fragment_with_multiple_inline_fragment_required_fields.graphql");
25+
let expected = include_str!("disallow_required_on_non_null_field/fixtures/fragment_with_multiple_inline_fragment_required_fields.expected");
26+
test_fixture(transform_fixture, file!(), "fragment_with_multiple_inline_fragment_required_fields.graphql", "disallow_required_on_non_null_field/fixtures/fragment_with_multiple_inline_fragment_required_fields.expected", input, expected).await;
27+
}
28+
2229
#[tokio::test]
2330
async fn fragment_with_multiple_required_non_null_fields_invalid() {
2431
let input = include_str!("disallow_required_on_non_null_field/fixtures/fragment_with_multiple_required_non_null_fields.invalid.graphql");

0 commit comments

Comments
 (0)