Skip to main content

apollo_smith/
fragment.rs

1use crate::description::Description;
2use crate::directive::Directive;
3use crate::directive::DirectiveLocation;
4use crate::name::Name;
5use crate::operation::OperationDef;
6use crate::selection_set::SelectionSet;
7use crate::ty::Ty;
8use crate::DocumentBuilder;
9use apollo_compiler::ast;
10use arbitrary::Result as ArbitraryResult;
11use indexmap::IndexMap;
12use indexmap::IndexSet;
13
14/// The __fragmentDef type represents a fragment definition
15///
16/// *FragmentDefinition*:
17///     Description? fragment FragmentName TypeCondition Directives? SelectionSet
18///
19/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/September2025/#FragmentDefinition).
20#[derive(Debug, Clone)]
21pub struct FragmentDef {
22    pub(crate) description: Option<Description>,
23    pub(crate) name: Name,
24    pub(crate) type_condition: TypeCondition,
25    pub(crate) directives: IndexMap<Name, Directive>,
26    pub(crate) selection_set: SelectionSet,
27}
28
29impl From<FragmentDef> for ast::Definition {
30    fn from(x: FragmentDef) -> Self {
31        ast::FragmentDefinition {
32            description: x.description.map(Into::into),
33            name: x.name.into(),
34            type_condition: x.type_condition.name.into(),
35            directives: Directive::to_ast(x.directives),
36            selection_set: x.selection_set.into(),
37        }
38        .into()
39    }
40}
41
42impl TryFrom<apollo_parser::cst::FragmentDefinition> for FragmentDef {
43    type Error = crate::FromError;
44
45    fn try_from(fragment_def: apollo_parser::cst::FragmentDefinition) -> Result<Self, Self::Error> {
46        Ok(Self {
47            description: fragment_def.description().map(Description::from),
48            name: fragment_def.fragment_name().unwrap().name().unwrap().into(),
49            directives: fragment_def
50                .directives()
51                .map(Directive::convert_directives)
52                .transpose()?
53                .unwrap_or_default(),
54            type_condition: fragment_def.type_condition().unwrap().into(),
55            selection_set: fragment_def.selection_set().unwrap().try_into()?,
56        })
57    }
58}
59
60/// The __fragmentSpread type represents a named fragment used in a selection set.
61///
62/// *FragmentSpread*:
63///     ... FragmentName Directives?
64///
65/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/September2025/#FragmentSpread).
66#[derive(Debug, Clone)]
67pub struct FragmentSpread {
68    pub(crate) name: Name,
69    pub(crate) directives: IndexMap<Name, Directive>,
70}
71
72impl From<FragmentSpread> for ast::FragmentSpread {
73    fn from(x: FragmentSpread) -> Self {
74        Self {
75            fragment_name: x.name.into(),
76            directives: Directive::to_ast(x.directives),
77        }
78    }
79}
80
81impl TryFrom<apollo_parser::cst::FragmentSpread> for FragmentSpread {
82    type Error = crate::FromError;
83
84    fn try_from(fragment_spread: apollo_parser::cst::FragmentSpread) -> Result<Self, Self::Error> {
85        Ok(Self {
86            name: fragment_spread
87                .fragment_name()
88                .unwrap()
89                .name()
90                .unwrap()
91                .into(),
92            directives: fragment_spread
93                .directives()
94                .map(Directive::convert_directives)
95                .transpose()?
96                .unwrap_or_default(),
97        })
98    }
99}
100
101/// The __inlineFragment type represents an inline fragment in a selection set that could be used as a field
102///
103/// *InlineFragment*:
104///     ... TypeCondition? Directives? SelectionSet
105///
106/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/September2025/#sec-Inline-Fragments).
107#[derive(Debug, Clone)]
108pub struct InlineFragment {
109    pub(crate) type_condition: Option<TypeCondition>,
110    pub(crate) directives: IndexMap<Name, Directive>,
111    pub(crate) selection_set: SelectionSet,
112}
113
114impl From<InlineFragment> for ast::InlineFragment {
115    fn from(x: InlineFragment) -> Self {
116        Self {
117            type_condition: x.type_condition.map(|t| t.name.into()),
118            directives: Directive::to_ast(x.directives),
119            selection_set: x.selection_set.into(),
120        }
121    }
122}
123
124impl TryFrom<apollo_parser::cst::InlineFragment> for InlineFragment {
125    type Error = crate::FromError;
126
127    fn try_from(inline_fragment: apollo_parser::cst::InlineFragment) -> Result<Self, Self::Error> {
128        Ok(Self {
129            directives: inline_fragment
130                .directives()
131                .map(Directive::convert_directives)
132                .transpose()?
133                .unwrap_or_default(),
134            selection_set: inline_fragment.selection_set().unwrap().try_into()?,
135            type_condition: inline_fragment.type_condition().map(TypeCondition::from),
136        })
137    }
138}
139
140/// The __typeCondition type represents where a fragment could be applied
141///
142/// *TypeCondition*:
143///     on NamedType
144///
145/// Detailed documentation can be found in [GraphQL spec](https://spec.graphql.org/September2025/#TypeCondition).
146#[derive(Debug, Clone)]
147pub struct TypeCondition {
148    name: Name,
149}
150
151impl From<apollo_parser::cst::TypeCondition> for TypeCondition {
152    fn from(type_condition: apollo_parser::cst::TypeCondition) -> Self {
153        Self {
154            name: type_condition.named_type().unwrap().name().unwrap().into(),
155        }
156    }
157}
158
159impl DocumentBuilder<'_> {
160    /// Create an arbitrary `FragmentDef`
161    pub fn fragment_definition(&mut self) -> ArbitraryResult<FragmentDef> {
162        // TODO: also choose between enum/scalars/object
163        let selected_object_type_name = self.u.choose(&self.object_type_defs)?.name.clone();
164        let _ = self.stack_ty(&Ty::Named(selected_object_type_name));
165        let name = self.type_name()?;
166        let directives = self.directives(DirectiveLocation::FragmentDefinition)?;
167        let selection_set = self.selection_set()?;
168        let type_condition = self.type_condition()?;
169        self.stack.pop();
170
171        Ok(FragmentDef {
172            description: None,
173            name,
174            type_condition,
175            directives,
176            selection_set,
177        })
178    }
179
180    /// Create an arbitrary `FragmentSpread`, returns `None` if no fragment definition was previously created
181    pub fn fragment_spread(
182        &mut self,
183        excludes: &mut Vec<Name>,
184    ) -> ArbitraryResult<Option<FragmentSpread>> {
185        let current_type = self.stack.last().map(|e| e.name().clone());
186        let available_fragment: Vec<&FragmentDef> = self
187            .fragment_defs
188            .iter()
189            .filter(|f| {
190                !excludes.contains(&f.name)
191                    && self.fragment_spread_possible(&f.type_condition.name, current_type.as_ref())
192            })
193            .collect();
194
195        let name = if available_fragment.is_empty() {
196            return Ok(None);
197        } else {
198            self.u.choose(&available_fragment)?.name.clone()
199        };
200        let directives = self.directives(DirectiveLocation::FragmentSpread)?;
201        excludes.push(name.clone());
202
203        Ok(Some(FragmentSpread { name, directives }))
204    }
205
206    /// Create an arbitrary `InlineFragment`
207    pub fn inline_fragment(&mut self) -> ArbitraryResult<InlineFragment> {
208        let type_condition = self
209            .u
210            .arbitrary()
211            .unwrap_or(false)
212            .then(|| self.type_condition())
213            .transpose()?;
214        let selection_set = self.selection_set()?;
215        let directives = self.directives(DirectiveLocation::InlineFragment)?;
216
217        Ok(InlineFragment {
218            type_condition,
219            directives,
220            selection_set,
221        })
222    }
223
224    /// Whether a fragment with `fragment_type` can be spread inside a
225    /// selection set for `current_type`. The two types must share at
226    /// least one possible object type.
227    ///
228    /// See <https://spec.graphql.org/September2025/#sec-Fragment-Spread-Is-Possible>.
229    fn fragment_spread_possible(&self, fragment_type: &Name, current_type: Option<&Name>) -> bool {
230        let Some(current) = current_type else {
231            return true;
232        };
233        let current_objects = self.possible_object_types(current);
234        let fragment_objects = self.possible_object_types(fragment_type);
235        current_objects.iter().any(|o| fragment_objects.contains(o))
236    }
237
238    /// The set of object types that `type_name` can resolve to at runtime.
239    fn possible_object_types(&self, type_name: &Name) -> IndexSet<Name> {
240        if self.object_type_defs.iter().any(|o| &o.name == type_name) {
241            return IndexSet::from([type_name.clone()]);
242        }
243        if let Some(u) = self.union_type_defs.iter().find(|u| &u.name == type_name) {
244            return u.members.clone();
245        }
246        // Interface: collect every object whose implements closure includes it
247        self.object_type_defs
248            .iter()
249            .filter(|o| self.implements_graph.closure(&o.name).contains(type_name))
250            .map(|o| o.name.clone())
251            .collect()
252    }
253
254    /// Create an arbitrary `TypeCondition`
255    pub fn type_condition(&mut self) -> ArbitraryResult<TypeCondition> {
256        let last_element = self.stack.last();
257        match last_element {
258            Some(last_element) => Ok(TypeCondition {
259                name: last_element.name().clone(),
260            }),
261            None => {
262                let named_types: Vec<Ty> = self
263                    .list_existing_object_types()
264                    .into_iter()
265                    .filter(Ty::is_named)
266                    .collect();
267
268                Ok(TypeCondition {
269                    name: self.choose_named_ty(&named_types)?.name().clone(),
270                })
271            }
272        }
273    }
274}
275
276/// Compute the set of fragment names reachable from `operations`, walking
277/// through `fragments` transitively when one fragment spreads another.
278///
279/// A fragment is reachable iff some operation spreads it directly, or spreads
280/// some other reachable fragment whose chain leads to it. Chains like
281/// `A -> B` with no operation referencing A produce no reachable names, even
282/// though `A` syntactically references `B`.
283pub(crate) fn reachable_fragment_names(
284    operations: &[OperationDef],
285    fragments: &[FragmentDef],
286) -> IndexSet<Name> {
287    let mut reachable: IndexSet<Name> = IndexSet::new();
288    for op in operations {
289        op.selection_set.collect_fragment_spreads(&mut reachable);
290    }
291    let mut frontier: Vec<Name> = reachable.iter().cloned().collect();
292    while let Some(name) = frontier.pop() {
293        if let Some(frag) = fragments.iter().find(|f| f.name == name) {
294            let mut nested: IndexSet<Name> = IndexSet::new();
295            frag.selection_set.collect_fragment_spreads(&mut nested);
296            for n in nested {
297                if reachable.insert(n.clone()) {
298                    frontier.push(n);
299                }
300            }
301        }
302    }
303    reachable
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    fn parse(src: &str) -> (Vec<OperationDef>, Vec<FragmentDef>) {
311        let cst = apollo_parser::Parser::new(src).parse();
312        assert!(cst.errors().next().is_none(), "parse errors: {src}");
313        let mut ops = vec![];
314        let mut frags = vec![];
315        for def in cst.document().definitions() {
316            match def {
317                apollo_parser::cst::Definition::OperationDefinition(o) => {
318                    ops.push(o.try_into().unwrap())
319                }
320                apollo_parser::cst::Definition::FragmentDefinition(f) => {
321                    frags.push(f.try_into().unwrap())
322                }
323                _ => panic!("unexpected definition in test input"),
324            }
325        }
326        (ops, frags)
327    }
328
329    fn names(items: &[&str]) -> IndexSet<Name> {
330        items.iter().map(|s| Name::new(s.to_string())).collect()
331    }
332
333    #[test]
334    fn no_operations_means_nothing_reachable() {
335        let (ops, frags) = parse("fragment A on T { __typename }");
336        let result = reachable_fragment_names(&ops, &frags);
337        assert!(result.is_empty());
338    }
339
340    #[test]
341    fn direct_spread_is_reachable() {
342        let (ops, frags) = parse(
343            "
344            query { ...A }
345            fragment A on T { __typename }
346            ",
347        );
348        let result = reachable_fragment_names(&ops, &frags);
349        assert_eq!(result, names(&["A"]));
350    }
351
352    #[test]
353    fn transitive_chain_is_reachable() {
354        let (ops, frags) = parse(
355            "
356            query { ...A }
357            fragment A on T { ...B }
358            fragment B on T { ...C }
359            fragment C on T { __typename }
360            ",
361        );
362        let result = reachable_fragment_names(&ops, &frags);
363        assert_eq!(result, names(&["A", "B", "C"]));
364    }
365
366    #[test]
367    fn orphan_chain_is_not_reachable() {
368        // A -> B with no operation referencing A: neither retained
369        let (ops, frags) = parse(
370            "
371            fragment A on T { ...B }
372            fragment B on T { __typename }
373            ",
374        );
375        let result = reachable_fragment_names(&ops, &frags);
376        assert!(result.is_empty());
377    }
378
379    #[test]
380    fn unreferenced_fragment_among_used_ones_is_pruned() {
381        let (ops, frags) = parse(
382            "
383            query { ...A }
384            fragment A on T { __typename }
385            fragment B on T { __typename }
386            ",
387        );
388        let result = reachable_fragment_names(&ops, &frags);
389        assert_eq!(result, names(&["A"]));
390    }
391
392    #[test]
393    fn cycle_terminates() {
394        // op -> A -> B -> A : both reachable, no infinite loop
395        let (ops, frags) = parse(
396            "
397            query { ...A }
398            fragment A on T { ...B }
399            fragment B on T { ...A }
400            ",
401        );
402        let result = reachable_fragment_names(&ops, &frags);
403        assert_eq!(result, names(&["A", "B"]));
404    }
405}