Skip to content

Commit 08b7496

Browse files
committed
add config for autoExhaustiveTypes
1 parent 6cf9c26 commit 08b7496

File tree

12 files changed

+221
-2121
lines changed

12 files changed

+221
-2121
lines changed

compiler/crates/relay-compiler/relay-compiler-config-schema.json

Lines changed: 41 additions & 2113 deletions
Large diffs are not rendered by default.

compiler/crates/relay-compiler/src/config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ impl Config {
424424
persist: config_file_project.persist,
425425
variable_names_comment: config_file_project.variable_names_comment,
426426
auto_exhaustive_mutations: config_file_project.auto_exhaustive_mutations,
427+
auto_exhaustive_types: config_file_project.auto_exhaustive_types.clone(),
427428
test_path_regex,
428429
feature_flags: Arc::new(
429430
config_file_project
@@ -1218,6 +1219,10 @@ pub struct ConfigFileProject {
12181219
#[serde(default)]
12191220
auto_exhaustive_mutations: bool,
12201221

1222+
/// Union/interface types that should be validated exhaustively without adding @exhaustive manually.
1223+
#[serde(default)]
1224+
auto_exhaustive_types: Vec<StringKey>,
1225+
12211226
/// A placeholder for allowing extra information in the config file
12221227
#[serde(default)]
12231228
extra: serde_json::Value,

compiler/crates/relay-config/src/project_config.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ pub struct ProjectConfig {
297297
/// Automatically enforce exhaustive union selections on mutation fields
298298
/// returning union types.
299299
pub auto_exhaustive_mutations: bool,
300+
/// GraphQL union or interface type names that should always be exhaustively validated.
301+
pub auto_exhaustive_types: Vec<StringKey>,
300302
/// Additional metadata for the project.
301303
pub extra: serde_json::Value,
302304
/// Feature flags for the project.
@@ -344,6 +346,7 @@ impl Default for ProjectConfig {
344346
persist: None,
345347
variable_names_comment: false,
346348
auto_exhaustive_mutations: false,
349+
auto_exhaustive_types: vec![],
347350
extra: Default::default(),
348351
test_path_regex: None,
349352
rollout: Default::default(),
@@ -379,6 +382,7 @@ impl Debug for ProjectConfig {
379382
persist,
380383
variable_names_comment,
381384
auto_exhaustive_mutations,
385+
auto_exhaustive_types,
382386
extra,
383387
feature_flags,
384388
test_path_regex,
@@ -410,6 +414,7 @@ impl Debug for ProjectConfig {
410414
.field("persist", persist)
411415
.field("variable_names_comment", variable_names_comment)
412416
.field("auto_exhaustive_mutations", auto_exhaustive_mutations)
417+
.field("auto_exhaustive_types", auto_exhaustive_types)
413418
.field("extra", extra)
414419
.field("feature_flags", feature_flags)
415420
.field("test_path_regex", test_path_regex)

compiler/crates/relay-schema/src/relay-extensions.graphql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,13 @@ Marks a field as exhaustive.
360360
"""
361361
directive @exhaustive(ignore: [String!], disabled: Boolean) on FIELD | FRAGMENT_DEFINITION
362362

363+
"""
364+
(RescriptRelay only)
365+
366+
Opt out of automatic exhaustive validation for a field or fragment.
367+
"""
368+
directive @nonExhaustive on FIELD | FRAGMENT_DEFINITION
369+
363370
"""
364371
(Relay Only)
365372

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

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use intern::string_key::Intern;
1818
use intern::string_key::StringKey;
1919
use lazy_static::lazy_static;
2020
use relay_config::ProjectConfig;
21+
use schema::FieldID;
2122
use schema::InterfaceID;
2223
use schema::ObjectID;
2324
use schema::SDLSchema;
@@ -29,6 +30,8 @@ use crate::ValidationMessage;
2930

3031
lazy_static! {
3132
pub static ref EXHAUSTIVE_DIRECTIVE_NAME: DirectiveName = DirectiveName("exhaustive".intern());
33+
pub static ref NON_EXHAUSTIVE_DIRECTIVE_NAME: DirectiveName =
34+
DirectiveName("nonExhaustive".intern());
3235
pub static ref IGNORE_ARG_NAME: ArgumentName = ArgumentName("ignore".intern());
3336
pub static ref DISABLED_ARG_NAME: ArgumentName = ArgumentName("disabled".intern());
3437
}
@@ -52,6 +55,7 @@ struct ExhaustiveDirectiveValidator<'schema, 'program, 'pc> {
5255
program: &'program Program,
5356
project_config: &'pc ProjectConfig,
5457
is_mutation: bool,
58+
auto_exhaustive_types: std::collections::HashSet<StringKey>,
5559
errors: Vec<Diagnostic>,
5660
}
5761

@@ -66,6 +70,11 @@ impl<'schema, 'program, 'pc> ExhaustiveDirectiveValidator<'schema, 'program, 'pc
6670
program,
6771
project_config,
6872
is_mutation: false,
73+
auto_exhaustive_types: project_config
74+
.auto_exhaustive_types
75+
.iter()
76+
.copied()
77+
.collect(),
6978
errors: vec![],
7079
}
7180
}
@@ -128,9 +137,8 @@ impl<'schema, 'program, 'pc> ExhaustiveDirectiveValidator<'schema, 'program, 'pc
128137
.recursively_implementing_objects(self.schema.as_ref())
129138
.into_iter()
130139
.collect::<Vec<_>>();
131-
implementing_objects.sort_by_key(|object_id| {
132-
self.schema.get_type_name(Type::Object(*object_id))
133-
});
140+
implementing_objects
141+
.sort_by_key(|object_id| self.schema.get_type_name(Type::Object(*object_id)));
134142

135143
let mut missing_members = Vec::new();
136144

@@ -151,18 +159,26 @@ impl<'schema, 'program, 'pc> ExhaustiveDirectiveValidator<'schema, 'program, 'pc
151159
let mut ignored = std::collections::HashSet::new();
152160
let mut has_directive = false;
153161
let mut disabled = false;
162+
let has_non_exhaustive = field
163+
.directives
164+
.named(*NON_EXHAUSTIVE_DIRECTIVE_NAME)
165+
.is_some();
154166

155167
if let Some(directive) = field.directives.named(*EXHAUSTIVE_DIRECTIVE_NAME) {
156168
has_directive = true;
157169
let (parsed_ignored, parsed_disabled) =
158170
Self::parse_exhaustive_directive_args(directive);
159171
ignored = parsed_ignored;
160172
disabled = parsed_disabled;
173+
} else if has_non_exhaustive {
174+
return;
161175
} else if self.project_config.auto_exhaustive_mutations
162176
&& self.is_mutation
163177
&& self.field_is_top_level_mutation(field)
164178
{
165179
has_directive = true;
180+
} else if self.field_return_type_is_auto_exhaustive(field.definition.item) {
181+
has_directive = true;
166182
}
167183

168184
if !has_directive || disabled {
@@ -213,13 +229,27 @@ impl<'schema, 'program, 'pc> ExhaustiveDirectiveValidator<'schema, 'program, 'pc
213229
}
214230

215231
fn validate_exhaustive_fragment(&mut self, fragment: &FragmentDefinition) {
216-
let Some(directive) = fragment.directives.named(*EXHAUSTIVE_DIRECTIVE_NAME) else {
217-
return;
218-
};
232+
let mut ignored = std::collections::HashSet::new();
233+
let mut disabled = false;
234+
let mut has_directive = false;
235+
let has_non_exhaustive = fragment
236+
.directives
237+
.named(*NON_EXHAUSTIVE_DIRECTIVE_NAME)
238+
.is_some();
219239

220-
let (ignored, disabled) = Self::parse_exhaustive_directive_args(directive);
240+
if let Some(directive) = fragment.directives.named(*EXHAUSTIVE_DIRECTIVE_NAME) {
241+
has_directive = true;
242+
let (parsed_ignored, parsed_disabled) =
243+
Self::parse_exhaustive_directive_args(directive);
244+
ignored = parsed_ignored;
245+
disabled = parsed_disabled;
246+
} else if has_non_exhaustive {
247+
return;
248+
} else if self.type_is_auto_exhaustive(fragment.type_condition) {
249+
has_directive = true;
250+
}
221251

222-
if disabled {
252+
if !has_directive || disabled {
223253
return;
224254
}
225255

@@ -321,6 +351,26 @@ impl<'schema, 'program, 'pc> ExhaustiveDirectiveValidator<'schema, 'program, 'pc
321351
_ => false,
322352
}
323353
}
354+
355+
fn type_is_auto_exhaustive(&self, type_: Type) -> bool {
356+
let Some(type_name) = self.get_auto_exhaustive_type_name(type_) else {
357+
return false;
358+
};
359+
self.auto_exhaustive_types.contains(&type_name)
360+
}
361+
362+
fn field_return_type_is_auto_exhaustive(&self, field_id: FieldID) -> bool {
363+
let return_type = self.schema.field(field_id).type_.inner();
364+
self.type_is_auto_exhaustive(return_type)
365+
}
366+
367+
fn get_auto_exhaustive_type_name(&self, type_: Type) -> Option<StringKey> {
368+
match type_ {
369+
Type::Union(id) => Some(self.schema.get_type_name(Type::Union(id))),
370+
Type::Interface(id) => Some(self.schema.get_type_name(Type::Interface(id))),
371+
_ => None,
372+
}
373+
}
324374
}
325375

326376
impl Validator for ExhaustiveDirectiveValidator<'_, '_, '_> {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77

88
use fixture_tests::Fixture;
99
use graphql_test_helpers::apply_transform_for_test;
10+
use intern::string_key::Intern;
1011
use relay_config::ProjectConfig;
1112
use relay_transforms::validate_exhaustive_directive;
1213

1314
pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String> {
1415
apply_transform_for_test(fixture, |program| {
16+
let auto_exhaustive_types = if fixture.file_name.contains("auto-type") {
17+
vec!["UserNameRenderer".intern()]
18+
} else {
19+
vec![]
20+
};
1521
let project_config = ProjectConfig {
1622
auto_exhaustive_mutations: true,
23+
auto_exhaustive_types,
1724
..Default::default()
1825
};
1926
validate_exhaustive_directive(program, &project_config)?;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
==================================== INPUT ====================================
2+
#expected-to-throw
3+
query AutoTypeMissing {
4+
me {
5+
nameRenderer {
6+
... on PlainUserNameRenderer { __typename }
7+
... on MarkdownUserNameRenderer { __typename }
8+
}
9+
}
10+
}
11+
==================================== ERROR ====================================
12+
✖︎ Field 'nameRenderer' marked with @exhaustive is missing selection for union members: 'CustomNameRenderer'.
13+
14+
auto-type-missing.invalid.graphql:4:5
15+
2 │ query AutoTypeMissing {
16+
3 │ me {
17+
4 │ nameRenderer {
18+
│ ^^^^^^^^^^^^
19+
5 │ ... on PlainUserNameRenderer { __typename }
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#expected-to-throw
2+
query AutoTypeMissing {
3+
me {
4+
nameRenderer {
5+
... on PlainUserNameRenderer { __typename }
6+
... on MarkdownUserNameRenderer { __typename }
7+
}
8+
}
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
==================================== INPUT ====================================
2+
query AutoTypeNonExhaustive {
3+
me {
4+
nameRenderer @nonExhaustive {
5+
... on PlainUserNameRenderer { __typename }
6+
}
7+
}
8+
}
9+
==================================== OUTPUT ===================================
10+
query AutoTypeNonExhaustive {
11+
me {
12+
nameRenderer @nonExhaustive {
13+
... on PlainUserNameRenderer { __typename }
14+
}
15+
}
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
query AutoTypeNonExhaustive {
2+
me {
3+
nameRenderer @nonExhaustive {
4+
... on PlainUserNameRenderer { __typename }
5+
}
6+
}
7+
}

0 commit comments

Comments
 (0)