in compiler/crates/relay-transforms/src/match_/match_transform.rs [254:520]
fn validate_transform_fragment_spread(
&mut self,
spread: &FragmentSpread,
) -> Result<Transformed<Selection>, Diagnostic> {
let module_directive = spread
.directives
.named(MATCH_CONSTANTS.module_directive_name);
// Only process the fragment spread with @module
if let Some(module_directive) = module_directive {
let should_use_no_inline = self.no_inline_flag.is_enabled_for(spread.fragment.item);
// @arguments on the fragment spread is not allowed without @no_inline
if !should_use_no_inline && !spread.arguments.is_empty() {
return Err(Diagnostic::error(
ValidationMessage::InvalidModuleWithArguments,
spread.arguments[0].name.location,
));
}
// no other directives are allowed
if spread.directives.len() != 1
&& !(spread.directives.len() == 2
&& spread
.directives
.named(DEFER_STREAM_CONSTANTS.defer_name)
.is_some())
{
// allow @defer and @module in typegen transforms
return Err(Diagnostic::error(
ValidationMessage::InvalidModuleWithAdditionalDirectives {
spread_name: spread.fragment.item,
},
spread.directives[1].name.location,
));
}
self.validate_js_module_type(spread.fragment.location)?;
let fragment = self.program.fragment(spread.fragment.item).unwrap();
// Disallow @inline on fragments whose spreads are decorated with @module
if let Some(inline_data_directive) = fragment.directives.named(*INLINE_DIRECTIVE_NAME) {
return Err(Diagnostic::error(
ValidationMessage::InvalidModuleWithInline,
module_directive.name.location,
)
.annotate(
"@inline directive location",
inline_data_directive.name.location,
));
}
let module_directive_name_argument =
get_module_directive_name_argument(module_directive, spread.fragment.location)?;
let (js_field_id, has_js_field_id_arg, has_js_field_branch_arg) =
self.get_js_field_args(fragment, spread)?;
let parent_name = self.path.last();
// self.match_directive_key_argument is the value passed to @match(key: "...") that we
// most recently encountered while traversing the operation, or the document name
let match_directive_key_argument = self
.match_directive_key_argument
.unwrap_or(self.document_name);
// If this is the first time we are encountering @module at this path, also ensure
// that we have not previously encountered another @module associated with the same
// match_directive_key_argument.
//
// This ensures that all of the @module's associated with a given @match occur at
// a single path.
let matches = match self.matches_for_path.get_mut(&self.path) {
None => {
let existing_match_with_key = self.matches_for_path.values().any(|entry| {
entry.match_directive_key_argument == match_directive_key_argument
});
if existing_match_with_key {
let parent_name = parent_name.expect("Cannot have @module selections at multiple paths unless the selections are within fields.");
return Err(Diagnostic::error(
ValidationMessage::InvalidModuleSelectionWithoutKey {
document_name: self.document_name,
parent_name: parent_name.item,
},
parent_name.location,
));
}
self.matches_for_path.insert(
self.path.clone(),
Matches {
match_directive_key_argument,
types: Default::default(),
},
);
self.matches_for_path.get_mut(&self.path).unwrap()
}
Some(matches) => matches,
};
if match_directive_key_argument != matches.match_directive_key_argument {
// The user can't override the key locally (per @module),
// so this is just an internal sanity check
panic!(
"Invalid @module selection: expected all selections at path '{:?} to have the same 'key', got '{}' and '{}'.",
&self.path, match_directive_key_argument, matches.match_directive_key_argument
);
}
// For each @module we encounter at this path, we also keep track of the type condition of the
// spread fragment (i.e. the type from which the fragment makes selections), and ensure that
// if we have multiple @module directives at the same path and type condition, they are
// exactly the same: i.e. are on a spread of the same fragment, and have the same
// value passed to name in @module(name: "...").
//
// This is required because, as currently set up, the resulting payload will have fields
// __typeName, __module_operation_FragmentName and __module_component_FragmentName.
// If multiple @module directives shared a fragment spread's type condition, but differed:
// - in which fragment was spread (resulting in a different __module_operation_FragmentName
// value in the response), or
// - in the @module(name: "...") parameter (resulting in a different
// __module_component_FragmentName value in the response)
// The server could nonetheless only return the correct 3D payload for one of the @module's.
let previous_match_for_type = matches.types.get(&fragment.type_condition);
if let Some(previous_match_for_type) = previous_match_for_type {
if previous_match_for_type.fragment.item != spread.fragment.item
|| previous_match_for_type.module_directive_name_argument
!= module_directive_name_argument
{
return Err(Diagnostic::error(
ValidationMessage::InvalidModuleSelectionMultipleMatches {
type_name: self.program.schema.get_type_name(fragment.type_condition),
alias_path: self
.path
.iter()
.map(|with_loc| with_loc.item.lookup())
.collect::<Vec<&str>>()
.join("."),
},
spread.fragment.location,
)
.annotate(
"related location",
previous_match_for_type.fragment.location,
));
}
} else {
matches.types.insert(
fragment.type_condition,
TypeMatch {
fragment: spread.fragment,
module_directive_name_argument,
},
);
}
// Done validating. Build out the resulting fragment spread.
let mut component_field_arguments = vec![build_string_literal_argument(
MATCH_CONSTANTS.js_field_module_arg,
module_directive_name_argument,
module_directive.name.location,
)];
let mut normalization_name = get_normalization_operation_name(spread.fragment.item);
normalization_name.push_str(".graphql");
let mut operation_field_arguments = vec![build_string_literal_argument(
MATCH_CONSTANTS.js_field_module_arg,
normalization_name.intern(),
module_directive.name.location,
)];
let module_id = if self.path.is_empty() {
self.document_name
} else {
let mut str = String::new();
str.push_str(self.document_name.lookup());
for path in &self.path {
str.push('.');
str.push_str(path.item.lookup());
}
str.intern()
};
if has_js_field_id_arg {
let id_arg = build_string_literal_argument(
MATCH_CONSTANTS.js_field_id_arg,
module_id,
module_directive.name.location,
);
component_field_arguments.push(id_arg.clone());
operation_field_arguments.push(id_arg);
}
if has_js_field_branch_arg && self.enable_3d_branch_arg_generation {
let branch_arg = build_string_literal_argument(
MATCH_CONSTANTS.js_field_branch_arg,
self.program
.schema
.as_ref()
.get_type_name(fragment.type_condition),
module_directive.name.location,
);
component_field_arguments.push(branch_arg.clone());
operation_field_arguments.push(branch_arg);
}
let component_field = Selection::ScalarField(Arc::new(ScalarField {
alias: Some(WithLocation::new(
module_directive.name.location,
format!("__module_component_{}", match_directive_key_argument).intern(),
)),
definition: WithLocation::new(module_directive.name.location, js_field_id),
arguments: component_field_arguments,
directives: Default::default(),
}));
let operation_field = Selection::ScalarField(Arc::new(ScalarField {
alias: Some(WithLocation::new(
module_directive.name.location,
format!("__module_operation_{}", match_directive_key_argument).intern(),
)),
definition: WithLocation::new(module_directive.name.location, js_field_id),
arguments: operation_field_arguments,
directives: Default::default(),
}));
let next_spread = Selection::FragmentSpread(Arc::new(FragmentSpread {
directives: spread
.directives
.iter()
.filter(|directive| {
directive.name.item != MATCH_CONSTANTS.module_directive_name
})
.cloned()
.collect(),
..spread.clone()
}));
if should_use_no_inline {
self.no_inline_fragments
.entry(fragment.name.item)
.or_insert_with(std::vec::Vec::new)
.push(self.document_name);
}
Ok(Transformed::Replace(Selection::InlineFragment(Arc::new(
InlineFragment {
type_condition: Some(fragment.type_condition),
directives: vec![],
selections: vec![Selection::InlineFragment(Arc::new(InlineFragment {
type_condition: Some(fragment.type_condition),
directives: vec![
ModuleMetadata {
key: match_directive_key_argument,
module_id,
module_name: module_directive_name_argument,
source_document_name: self.document_name,
fragment_name: spread.fragment.item,
location: module_directive.name.location,
no_inline: should_use_no_inline,
}
.into(),
],
selections: vec![next_spread, operation_field, component_field],
}))],
},
))))
} else {
Ok(Transformed::Keep)
}
}