Repository: apollographql/apollo-rs
Pull request #482 (2023-03-14 14:19:45)

feat(compiler): implicit extensions in HIR uniquely-named-item APIs
Part of https://github.com/apollographql/apollo-rs/issues/468, for components that have a unique name such as fields of object types.

Internally, `IndexMap` is used to map names to `Vec` indices.

Renamed:

* `ObjectTypeDefinition::fields_definition` → `self_fields`
* `ObjectTypeDefinition::implements_interfaces` → `self_implements_interfaces`
* `InterfaceTypeDefinition::fields_definition` → `self_fields`
* `InterfaceTypeDefinition::implements_interfaces` → `self_implements_interfaces`
* `InputObjectTypeDefinition::input_fields_definition` → `self_fields`
* `UnionTypeDefinition::union_members` → `self_members`
* `EnumTypeDefinition::enum_values_definition` → `self_values`

Names freed by the above, redefined with new behavior (to consider extensions) and signature:

* `ObjectTypeDefinition::implements_interfaces() -> impl Iterator`
* `InterfaceTypeDefinition::implements_interfaces() -> impl Iterator`

Behavior changed to consider extensions (no signature change):

* `TypeDefinition::field(name) -> Option`
* `ObjectTypeDefinition::field(name) -> Option`
* `InterfaceTypeDefinition::field(name) -> Option`

New APIs (that all consider extensions):

* `ObjectTypeDefinition::fields() -> impl Iterator`
* `ObjectTypeDefinition::implements_interface(name) -> bool`
* `InterfaceTypeDefinition::fields() -> impl Iterator`
* `InterfaceTypeDefinition::implements_interface(name) -> bool`
* `InputObjectTypeDefinition::self_fields() -> &[_]`
* `InputObjectTypeDefinition::fields() -> impl Iterator`
* `InputObjectTypeDefinition::field(name) -> Option`
* `UnionTypeDefinition::members() -> impl Iterator`
* `UnionTypeDefinition::has_member(name) -> bool`
* `EnumTypeDefinition::values() -> impl Iterator`
* `EnumTypeDefinition::value(name) -> Option`

SimonSapin: Draft PR for early feedback on this pattern applied to object type fields. TODO: apply the same pattern to everything in extensions with unique names:

* interface fields
* input object type fields
* object type implemented interfaces
* interface type implemented interfaces
* union members
* enum values

SimonSapin: I think this is now complete but it felt rather prone to copy-paste errors (doc-comments especially). I’ll read through it again in a few hours after clearing my mind.

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index 2bb7318..277f120 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -23,10 +23,10 @@ This release has a few breaking changes as we try to standardise APIs across the
 compiler. We appreciate your patience with these changes. If you run into trouble, please [open an issue].
 
 ## BREAKING
-- Rename `compiler.create_*` methods to `compiler.add_*`, [SimonSapin] in [412]
+- Rename `compiler.create_*` methods to `compiler.add_*`, [SimonSapin] in [pull/412]
 - Rename `schema` to `type_system` for `compiler.add_` and `compiler.update_`
-methods, [SimonSapin] in [413]
-- Unify `ty`, `type_def` and `kind` namings in HIR, [lrlna] in [415]
+methods, [SimonSapin] in [pull/413]
+- Unify `ty`, `type_def` and `kind` namings in HIR, [lrlna] in [pull/415]
   - in `Type` struct impl: `ty()` --> `type_def()`
   - in `TypeDefinition` struct impl: `ty()` --> `kind()`
   - in `FragmentDefinition` struct impl: `ty()` --> `type_def()`
diff --git a/crates/apollo-compiler/src/database/document.rs b/crates/apollo-compiler/src/database/document.rs
index 3363788..953c9b5 100644
--- a/crates/apollo-compiler/src/database/document.rs
+++ b/crates/apollo-compiler/src/database/document.rs
@@ -211,6 +211,41 @@ pub(crate) fn operation_fragment_spread_fields(
     Arc::new(fields)
 }
 
+pub(crate) fn flattened_operation_fields(
+    db: &dyn HirDatabase,
+    selection_set: SelectionSet,
+) -> Vec<Arc<Field>> {
+    fn flatten_selection_set(
+        db: &dyn HirDatabase,
+        selection_set: &SelectionSet,
+        seen: &mut HashSet<SelectionSet>,
+    ) -> Vec<Arc<Field>> {
+        if seen.contains(selection_set) {
+            return vec![];
+        }
+        seen.insert(selection_set.clone());
+
+        selection_set
+            .selection()
+            .iter()
+            .flat_map(|sel| match sel {
+                Selection::Field(field) => {
+                    vec![Arc::clone(field)]
+                }
+                Selection::FragmentSpread(fragment_spread) => fragment_spread
+                    .fragment(db)
+                    .map(|fragment| flatten_selection_set(db, fragment.selection_set(), seen))
+                    .unwrap_or_default(),
+                Selection::InlineFragment(fragment_spread) => {
+                    flatten_selection_set(db, fragment_spread.selection_set(), seen)
+                }
+            })
+            .collect()
+    }
+
+    flatten_selection_set(db, &selection_set, &mut HashSet::new())
+}
+
 // Should be part of operation's db
 // NOTE: potentially want to return a hashmap of variables and their types?
 pub(crate) fn selection_variables(
@@ -275,7 +310,7 @@ pub(crate) fn subtype_map(db: &dyn HirDatabase) -> Arc<HashMap<String, HashSet<S
             .insert(value.to_owned())
     };
     for (name, definition) in &*db.object_types() {
-        for implements in definition.implements_interfaces() {
+        for implements in definition.self_implements_interfaces() {
             add(implements.interface(), name);
         }
         for extension in definition.extensions() {
@@ -285,7 +320,7 @@ pub(crate) fn subtype_map(db: &dyn HirDatabase) -> Arc<HashMap<String, HashSet<S
         }
     }
     for (name, definition) in &*db.interfaces() {
-        for implements in definition.implements_interfaces() {
+        for implements in definition.self_implements_interfaces() {
             add(implements.interface(), name);
         }
         for extension in definition.extensions() {
@@ -295,7 +330,7 @@ pub(crate) fn subtype_map(db: &dyn HirDatabase) -> Arc<HashMap<String, HashSet<S
         }
     }
     for (name, definition) in &*db.unions() {
-        for member in definition.union_members() {
+        for member in definition.self_members() {
             add(name, member.name());
         }
         for extension in definition.extensions() {
diff --git a/crates/apollo-compiler/src/database/hir.rs b/crates/apollo-compiler/src/database/hir.rs
index b612ce8..38739cf 100644
--- a/crates/apollo-compiler/src/database/hir.rs
+++ b/crates/apollo-compiler/src/database/hir.rs
@@ -1,6 +1,6 @@
 use std::{
     collections::{HashMap, HashSet},
-    fmt,
+    fmt, hash,
     sync::Arc,
 };
 
@@ -84,6 +84,17 @@ impl TypeDefinition {
         }
     }
 
+    /// Returns whether this definition is a composite definition (union, interface, or object).
+    #[must_use]
+    pub fn is_composite_definition(&self) -> bool {
+        matches!(
+            self,
+            Self::ObjectTypeDefinition(_)
+                | Self::InterfaceTypeDefinition(_)
+                | Self::UnionTypeDefinition(_)
+        )
+    }
+
     /// Returns whether this definition is a scalar, object, interface, union, or enum.
     #[must_use]
     pub fn is_output_definition(&self) -> bool {
@@ -391,12 +402,10 @@ impl FragmentDefinition {
     // which operation definition the fragment is used in.
 
     /// Get variables used in a fragment definition.
+    ///
+    /// TODO(@goto-bus-stop): Maybe rename this to used_variables
     pub fn variables(&self, db: &dyn HirDatabase) -> Vec<Variable> {
-        self.selection_set
-            .selection()
-            .iter()
-            .flat_map(|sel| sel.variables(db))
-            .collect()
+        self.selection_set.variables(db)
     }
 
     pub fn type_def(&self, db: &dyn HirDatabase) -> Option<TypeDefinition> {
@@ -729,6 +738,16 @@ impl Type {
     }
 }
 
+impl std::fmt::Display for Type {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Type::NonNull { ty, .. } => write!(f, "{ty}!"),
+            Type::List { ty, .. } => write!(f, "[{ty}]"),
+            Type::Named { name, .. } => write!(f, "{name}"),
+        }
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct Directive {
     pub(crate) name: Name,
@@ -971,6 +990,31 @@ impl Value {
     pub fn is_variable(&self) -> bool {
         matches!(self, Self::Variable(..))
     }
+
+    /// Returns `true` if `other` represents the same value as `self`. This is different from the
+    /// `Eq` implementation as it ignores location information.
+    pub fn is_same_value(&self, other: &Value) -> bool {
+        match (self, other) {
+            (Value::Variable(left), Value::Variable(right)) => left.name() == right.name(),
+            (Value::Int(left) | Value::Float(left), Value::Int(right) | Value::Float(right)) => {
+                left == right
+            }
+            (Value::String(left), Value::String(right)) => left == right,
+            (Value::Boolean(left), Value::Boolean(right)) => left == right,
+            (Value::Null, Value::Null) => true,
+            (Value::Enum(left), Value::Enum(right)) => left.src() == right.src(),
+            (Value::List(left), Value::List(right)) if left.len() == right.len() => left
+                .iter()
+                .zip(right)
+                .all(|(left, right)| left.is_same_value(right)),
+            (Value::Object(left), Value::Object(right)) if left.len() == right.len() => {
+                left.iter().zip(right).all(|(left, right)| {
+                    left.0.src() == left.0.src() && left.1.is_same_value(&right.1)
+                })
+            }
+            _ => false,
+        }
+    }
 }
 
 /// Coerce to a `Float` input type (from either `Float` or `Int` syntax)
@@ -1134,6 +1178,48 @@ impl SelectionSet {
         })
     }
 
+    /// Get all variables used in this selection set.
+    pub fn variables(&self, db: &dyn HirDatabase) -> Vec<Variable> {
+        /// Recursively collect used variables. Accounts for self-referential fragments.
+        fn collect_used_variables(
+            db: &dyn HirDatabase,
+            set: &SelectionSet,
+            seen_fragments: &mut HashSet<Arc<FragmentDefinition>>,
+            output: &mut Vec<Variable>,
+        ) {
+            for selection in set.selection() {
+                match selection {
+                    Selection::Field(field) => {
+                        output.extend(field.self_used_variables());
+                        collect_used_variables(db, field.selection_set(), seen_fragments, output);
+                    }
+                    Selection::FragmentSpread(spread) => {
+                        let Some(fragment) = spread.fragment(db) else {
+                            return;
+                        };
+                        if seen_fragments.contains(&fragment) {
+                            return; // prevent recursion loop
+                        }
+                        seen_fragments.insert(Arc::clone(&fragment));
+                        collect_used_variables(
+                            db,
+                            fragment.selection_set(),
+                            seen_fragments,
+                            output,
+                        );
+                    }
+                    Selection::InlineFragment(inline) => {
+                        collect_used_variables(db, inline.selection_set(), seen_fragments, output);
+                    }
+                }
+            }
+        }
+
+        let mut output = vec![];
+        collect_used_variables(db, self, &mut HashSet::new(), &mut output);
+        output
+    }
+
     /// Returns true if all the [`Selection`]s in this selection set are themselves introspections.
     pub fn is_introspection(&self, db: &dyn HirDatabase) -> bool {
         self.selection().iter().all(|selection| match selection {
@@ -1142,6 +1228,21 @@ impl SelectionSet {
             Selection::InlineFragment(inline) => inline.is_introspection(db),
         })
     }
+
+    /// Create a selection set for the concatenation of two selection sets' fields.
+    ///
+    /// This does not deduplicate fields: if the two selection sets both select a field `a`, the
+    /// merged set will select field `a` twice.
+    pub fn merge(&self, other: &SelectionSet) -> SelectionSet {
+        let mut merged: Vec<Selection> =
+            Vec::with_capacity(self.selection.len() + other.selection.len());
+        merged.append(&mut self.selection.as_ref().clone());
+        merged.append(&mut other.selection.as_ref().clone());
+
+        SelectionSet {
+            selection: Arc::new(merged),
+        }
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -1183,6 +1284,15 @@ impl Selection {
     pub fn is_inline_fragment(&self) -> bool {
         matches!(self, Self::InlineFragment(..))
     }
+
+    /// Get the AST location information for this HIR node.
+    pub fn loc(&self) -> HirNodeLocation {
+        match self {
+            Selection::Field(field) => field.loc(),
+            Selection::FragmentSpread(fragment_spread) => fragment_spread.loc(),
+            Selection::InlineFragment(inline_fragment) => inline_fragment.loc(),
+        }
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -1205,11 +1315,31 @@ impl Field {
         }
     }
 
-    /// Get a reference to the field's name.
+    /// Get the field's name, corresponding to the definition it looks up.
+    ///
+    /// For example, in this operation, the `.name()` is "sourceField":
+    /// ```graphql
+    /// query GetField { alias: sourceField }
+    /// ```
     pub fn name(&self) -> &str {
         self.name.src()
     }
 
+    /// Get the name that will be used for this field selection in response formatting.
+    ///
+    /// For example, in this operation, the `.response_name()` is "sourceField":
+    /// ```graphql
+    /// query GetField { sourceField }
+    /// ```
+    ///
+    /// But in this operation that uses an alias, the `.response_name()` is "responseField":
+    /// ```graphql
+    /// query GetField { responseField: sourceField }
+    /// ```
+    pub fn response_name(&self) -> &str {
+        self.alias().map(Alias::name).unwrap_or_else(|| self.name())
+    }
+
     /// Get a reference to field's type.
     pub fn ty(&self, db: &dyn HirDatabase) -> Option<Type> {
         let def = db
@@ -1220,10 +1350,15 @@ impl Field {
         Some(def)
     }
 
+    /// Get the field's parent type definition.
+    pub fn parent_type(&self, db: &dyn HirDatabase) -> Option<TypeDefinition> {
+        db.find_type_definition_by_name(self.parent_obj.as_ref()?.to_string())
+    }
+
     /// Get field's original field definition.
     pub fn field_definition(&self, db: &dyn HirDatabase) -> Option<FieldDefinition> {
         db.find_object_type_by_name(self.parent_obj.as_ref()?.to_string())?
-            .fields_definition()
+            .self_fields()
             .iter()
             .find(|field| field.name() == self.name())
             .cloned()
@@ -1263,22 +1398,28 @@ impl Field {
         &self.selection_set
     }
 
-    /// Get variables used in the field.
+    /// Return an iterator over the variables used in arguments to this field.
+    fn self_used_variables(&self) -> impl Iterator<Item = Variable> + '_ {
+        self.arguments.iter().filter_map(|arg| match arg.value() {
+            Value::Variable(var) => Some(var.clone()),
+            _ => None,
+        })
+    }
+
+    /// Get variables used in the field, including in sub-selections.
+    ///
+    /// For example, with this field:
+    /// ```graphql
+    /// {
+    ///   field(arg: $arg) {
+    ///     number(formatAs: $format)
+    ///   }
+    /// }
+    /// ```
+    /// the used variables are `$arg` and `$format`.
     pub fn variables(&self, db: &dyn HirDatabase) -> Vec<Variable> {
-        let mut vars: Vec<_> = self
-            .arguments
-            .iter()
-            .filter_map(|arg| match arg.value() {
-                Value::Variable(var) => Some(var.clone()),
-                _ => None,
-            })
-            .collect();
-        let iter = self
-            .selection_set
-            .selection()
-            .iter()
-            .flat_map(|sel| sel.variables(db));
-        vars.extend(iter);
+        let mut vars = self.self_used_variables().collect::<Vec<_>>();
+        vars.extend(self.selection_set.variables(db));
         vars
     }
 
@@ -1339,13 +1480,7 @@ impl InlineFragment {
 
     /// Get variables in use in the inline fragment.
     pub fn variables(&self, db: &dyn HirDatabase) -> Vec<Variable> {
-        let vars = self
-            .selection_set
-            .selection()
-            .iter()
-            .flat_map(|sel| sel.variables(db))
-            .collect();
-        vars
+        self.selection_set.variables(db)
     }
 
     /// Get the AST location information for this HIR node.
@@ -1379,16 +1514,9 @@ impl FragmentSpread {
 
     /// Get fragment spread's defined variables.
     pub fn variables(&self, db: &dyn HirDatabase) -> Vec<Variable> {
-        let vars = match self.fragment(db) {
-            Some(fragment) => fragment
-                .selection_set
-                .selection()
-                .iter()
-                .flat_map(|sel| sel.variables(db))
-                .collect(),
-            None => Vec::new(),
-        };
-        vars
+        self.fragment(db)
+            .map(|fragment| fragment.variables(db))
+            .unwrap_or_default()
     }
 
     /// Get a reference to fragment spread directives.
@@ -1431,6 +1559,11 @@ impl FragmentSpread {
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct Alias(pub String);
+impl Alias {
+    pub fn name(&self) -> &str {
+        &self.0
+    }
+}
 
 #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
 pub struct Float {
@@ -1463,6 +1596,92 @@ impl Float {
     }
 }
 
+/// When some item (for example fields of an object type) have unique names
+/// and may be found on type extensions, this pre-computes where to find
+/// each item from its name
+#[derive(Clone, Debug, Eq)]
+pub(crate) struct ByNameWithExtensions {
+    /// `(None, i)` designates `def.example[i]`.
+    /// `(Some(j), i)` designates `def.extensions[j].example[i]`.
+    indices: IndexMap<String, (Option<usize>, usize)>,
+}
+
+/// Equivalent to ignoring a `ByNameWithExtensions` field in `PartialEq` for its parent struct,
+/// since it is determined by other fields.
+impl PartialEq for ByNameWithExtensions {
+    fn eq(&self, _other: &Self) -> bool {
+        true
+    }
+}
+
+/// Equivalent to ignoring a `ByNameWithExtensions` field in `Hash` for its parent struct,
+/// since it is determined by other fields.
+impl hash::Hash for ByNameWithExtensions {
+    fn hash<H: hash::Hasher>(&self, _state: &mut H) {
+        // do nothing
+    }
+}
+
+impl ByNameWithExtensions {
+    pub(crate) fn new<Item>(self_items: &[Item], name: impl Fn(&Item) -> &str) -> Self {
+        let mut indices = IndexMap::new();
+        for (i, item) in self_items.iter().enumerate() {
+            indices.entry(name(item).to_owned()).or_insert((None, i));
+        }
+        ByNameWithExtensions { indices }
+    }
+
+    pub(crate) fn add_extension<Item>(
+        &mut self,
+        extension_index: usize,
+        extension_items: &[Item],
+        name: impl Fn(&Item) -> &str,
+    ) {
+        for (i, item) in extension_items.iter().enumerate() {
+            self.indices
+                .entry(name(item).to_owned())
+                .or_insert((Some(extension_index), i));
+        }
+    }
+
+    fn get_by_index<'a, Item, Ext>(
+        &self,
+        (ext, i): (Option<usize>, usize),
+        self_items: &'a [Item],
+        extensions: &'a [Arc<Ext>],
+        extension_items: impl Fn(&'a Ext) -> &'a [Item],
+    ) -> &'a Item {
+        let items = if let Some(j) = ext {
+            extension_items(&extensions[j])
+        } else {
+            self_items
+        };
+        &items[i]
+    }
+
+    pub(crate) fn get<'a, Item, Ext>(
+        &self,
+        name: &str,
+        self_items: &'a [Item],
+        extensions: &'a [Arc<Ext>],
+        extension_items: impl Fn(&'a Ext) -> &'a [Item],
+    ) -> Option<&'a Item> {
+        let index = *self.indices.get(name)?;
+        Some(self.get_by_index(index, self_items, extensions, extension_items))
+    }
+
+    pub(crate) fn iter<'a, Item, Ext>(
+        &'a self,
+        self_items: &'a [Item],
+        extensions: &'a [Arc<Ext>],
+        extension_items: impl Fn(&'a Ext) -> &'a [Item] + Copy + 'a,
+    ) -> impl Iterator<Item = &'a Item> + ExactSizeIterator + DoubleEndedIterator {
+        self.indices
+            .values()
+            .map(move |&index| self.get_by_index(index, self_items, extensions, extension_items))
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Default, Eq)]
 pub struct SchemaDefinition {
     pub(crate) description: Option<String>,
@@ -1473,6 +1692,10 @@ pub struct SchemaDefinition {
 }
 
 impl SchemaDefinition {
+    pub fn description(&self) -> Option<&str> {
+        self.description.as_deref()
+    }
+
     /// Get a reference to the schema definition's directives (excluding those on extensions).
     pub fn self_directives(&self) -> &[Directive] {
         self.directives.as_ref()
@@ -1619,6 +1842,8 @@ pub struct ObjectTypeDefinition {
     pub(crate) fields_definition: Arc<Vec<FieldDefinition>>,
     pub(crate) loc: HirNodeLocation,
     pub(crate) extensions: Vec<Arc<ObjectTypeExtension>>,
+    pub(crate) fields_by_name: ByNameWithExtensions,
+    pub(crate) implements_interfaces_by_name: ByNameWithExtensions,
 }
 
 impl ObjectTypeDefinition {
@@ -1673,21 +1898,65 @@ impl ObjectTypeDefinition {
             .filter(move |directive| directive.name() == name)
     }
 
-    /// Get a reference to the object type definition's field definitions.
-    pub fn fields_definition(&self) -> &[FieldDefinition] {
+    /// Get a reference to the object type definition's field definitions,
+    /// excluding fields from extensions.
+    pub fn self_fields(&self) -> &[FieldDefinition] {
         self.fields_definition.as_ref()
     }
 
-    /// Find a field in object type definition.
+    /// Returns an iterator of fields of this object type,
+    /// whether from its own definition or from extensions.
+    pub fn fields(
+        &self,
+    ) -> impl Iterator<Item = &FieldDefinition> + ExactSizeIterator + DoubleEndedIterator {
+        self.fields_by_name.iter(
+            self.self_fields(),
+            self.extensions(),
+            ObjectTypeExtension::fields_definition,
+        )
+    }
+
+    /// Find a field by its name, either in this object type definition or its extensions.
     pub fn field(&self, name: &str) -> Option<&FieldDefinition> {
-        self.fields_definition().iter().find(|f| f.name() == name)
+        self.fields_by_name.get(
+            name,
+            self.self_fields(),
+            self.extensions(),
+            ObjectTypeExtension::fields_definition,
+        )
     }
 
-    /// Get a reference to object type definition's implements interfaces vector.
-    pub fn implements_interfaces(&self) -> &[ImplementsInterface] {
+    /// Returns interfaces implemented by this object type definition,
+    /// excluding those from extensions.
+    pub fn self_implements_interfaces(&self) -> &[ImplementsInterface] {
         self.implements_interfaces.as_ref()
     }
 
+    /// Returns an iterator of interfaces implemented by this object type,
+    /// whether from its own definition or from extensions.
+    pub fn implements_interfaces(
+        &self,
+    ) -> impl Iterator<Item = &ImplementsInterface> + ExactSizeIterator + DoubleEndedIterator {
+        self.implements_interfaces_by_name.iter(
+            self.self_implements_interfaces(),
+            self.extensions(),
+            ObjectTypeExtension::implements_interfaces,
+        )
+    }
+
+    /// Returns whether this object type implements the interface of the given name,
+    /// either in its own definition or its extensions.
+    pub fn implements_interface(&self, name: &str) -> bool {
+        self.implements_interfaces_by_name
+            .get(
+                name,
+                self.self_implements_interfaces(),
+                self.extensions(),
+                ObjectTypeExtension::implements_interfaces,
+            )
+            .is_some()
+    }
+
     /// Get the AST location information for this HIR node.
     pub fn loc(&self) -> HirNodeLocation {
         self.loc
@@ -1697,6 +1966,21 @@ impl ObjectTypeDefinition {
     pub fn extensions(&self) -> &[Arc<ObjectTypeExtension>] {
         &self.extensions
     }
+
+    pub(crate) fn push_extension(&mut self, ext: Arc<ObjectTypeExtension>) {
+        let next_index = self.extensions.len();
+        self.fields_by_name.add_extension(
+            next_index,
+            ext.fields_definition(),
+            FieldDefinition::name,
+        );
+        self.implements_interfaces_by_name.add_extension(
+            next_index,
+            ext.implements_interfaces(),
+            ImplementsInterface::interface,
+        );
+        self.extensions.push(ext);
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -1949,6 +2233,10 @@ impl ScalarTypeDefinition {
     pub fn extensions(&self) -> &[Arc<ScalarTypeExtension>] {
         &self.extensions
     }
+
+    pub(crate) fn push_extension(&mut self, ext: Arc<ScalarTypeExtension>) {
+        self.extensions.push(ext);
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -1959,6 +2247,7 @@ pub struct EnumTypeDefinition {
     pub(crate) enum_values_definition: Arc<Vec<EnumValueDefinition>>,
     pub(crate) loc: HirNodeLocation,
     pub(crate) extensions: Vec<Arc<EnumTypeExtension>>,
+    pub(crate) values_by_name: ByNameWithExtensions,
 }
 
 impl EnumTypeDefinition {
@@ -2013,11 +2302,33 @@ impl EnumTypeDefinition {
             .filter(move |directive| directive.name() == name)
     }
 
-    /// Get a reference to enum definition's enum values definition vector.
-    pub fn enum_values_definition(&self) -> &[EnumValueDefinition] {
+    /// Returns the values of this enum definition, excluding those from extensions.
+    pub fn self_values(&self) -> &[EnumValueDefinition] {
         self.enum_values_definition.as_ref()
     }
 
+    /// Returns an iterator of values of this enum type,
+    /// whether from its own definition or from extensions.
+    pub fn values(
+        &self,
+    ) -> impl Iterator<Item = &EnumValueDefinition> + ExactSizeIterator + DoubleEndedIterator {
+        self.values_by_name.iter(
+            self.self_values(),
+            self.extensions(),
+            EnumTypeExtension::enum_values_definition,
+        )
+    }
+
+    /// Find an enum value by its name, either in this enum type definition or its extensions.
+    pub fn value(&self, name: &str) -> Option<&EnumValueDefinition> {
+        self.values_by_name.get(
+            name,
+            self.self_values(),
+            self.extensions(),
+            EnumTypeExtension::enum_values_definition,
+        )
+    }
+
     /// Get the AST location information for this HIR node.
     pub fn loc(&self) -> HirNodeLocation {
         self.loc
@@ -2027,6 +2338,16 @@ impl EnumTypeDefinition {
     pub fn extensions(&self) -> &[Arc<EnumTypeExtension>] {
         &self.extensions
     }
+
+    pub(crate) fn push_extension(&mut self, ext: Arc<EnumTypeExtension>) {
+        let next_index = self.extensions.len();
+        self.values_by_name.add_extension(
+            next_index,
+            ext.enum_values_definition(),
+            EnumValueDefinition::enum_value,
+        );
+        self.extensions.push(ext);
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -2038,6 +2359,10 @@ pub struct EnumValueDefinition {
 }
 
 impl EnumValueDefinition {
+    pub fn description(&self) -> Option<&str> {
+        self.description.as_deref()
+    }
+
     /// Get a reference to enum value definition's enum value
     pub fn enum_value(&self) -> &str {
         self.enum_value.src()
@@ -2081,6 +2406,7 @@ pub struct UnionTypeDefinition {
     pub(crate) union_members: Arc<Vec<UnionMember>>,
     pub(crate) loc: HirNodeLocation,
     pub(crate) extensions: Vec<Arc<UnionTypeExtension>>,
+    pub(crate) members_by_name: ByNameWithExtensions,
 }
 
 impl UnionTypeDefinition {
@@ -2135,11 +2461,37 @@ impl UnionTypeDefinition {
             .filter(move |directive| directive.name() == name)
     }
 
-    /// Get a reference to union definition's union members.
-    pub fn union_members(&self) -> &[UnionMember] {
+    /// Get a reference to union definition's union members,
+    /// excluding those from extensions.
+    pub fn self_members(&self) -> &[UnionMember] {
         self.union_members.as_ref()
     }
 
+    /// Returns an iterator of members of this union type,
+    /// whether from its own definition or from extensions.
+    pub fn members(
+        &self,
+    ) -> impl Iterator<Item = &UnionMember> + ExactSizeIterator + DoubleEndedIterator {
+        self.members_by_name.iter(
+            self.self_members(),
+            self.extensions(),
+            UnionTypeExtension::union_members,
+        )
+    }
+
+    /// Returns whether the type of the given name is a member of this union type,
+    /// either from the union type definition or its extensions.
+    pub fn has_member(&self, name: &str) -> bool {
+        self.members_by_name
+            .get(
+                name,
+                self.self_members(),
+                self.extensions(),
+                UnionTypeExtension::union_members,
+            )
+            .is_some()
+    }
+
     /// Get the AST location information for this HIR node.
     pub fn loc(&self) -> HirNodeLocation {
         self.loc
@@ -2149,6 +2501,13 @@ impl UnionTypeDefinition {
     pub fn extensions(&self) -> &[Arc<UnionTypeExtension>] {
         &self.extensions
     }
+
+    pub(crate) fn push_extension(&mut self, ext: Arc<UnionTypeExtension>) {
+        let next_index = self.extensions.len();
+        self.members_by_name
+            .add_extension(next_index, ext.union_members(), UnionMember::name);
+        self.extensions.push(ext);
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -2183,6 +2542,8 @@ pub struct InterfaceTypeDefinition {
     pub(crate) fields_definition: Arc<Vec<FieldDefinition>>,
     pub(crate) loc: HirNodeLocation,
     pub(crate) extensions: Vec<Arc<InterfaceTypeExtension>>,
+    pub(crate) fields_by_name: ByNameWithExtensions,
+    pub(crate) implements_interfaces_by_name: ByNameWithExtensions,
 }
 
 impl InterfaceTypeDefinition {
@@ -2201,11 +2562,37 @@ impl InterfaceTypeDefinition {
         self.description.as_deref()
     }
 
-    /// Get a reference to interface definition's implements interfaces vector.
-    pub fn implements_interfaces(&self) -> &[ImplementsInterface] {
+    /// Returns interfaces implemented by this interface type definition,
+    /// excluding those from extensions.
+    pub fn self_implements_interfaces(&self) -> &[ImplementsInterface] {
         self.implements_interfaces.as_ref()
     }
 
+    /// Returns an iterator of interfaces implemented by this interface type,
+    /// whether from its own definition or from extensions.
+    pub fn implements_interfaces(
+        &self,
+    ) -> impl Iterator<Item = &ImplementsInterface> + ExactSizeIterator + DoubleEndedIterator {
+        self.implements_interfaces_by_name.iter(
+            self.self_implements_interfaces(),
+            self.extensions(),
+            InterfaceTypeExtension::implements_interfaces,
+        )
+    }
+
+    /// Returns whether this interface type implements the interface of the given name,
+    /// either in its own definition or its extensions.
+    pub fn implements_interface(&self, name: &str) -> bool {
+        self.implements_interfaces_by_name
+            .get(
+                name,
+                self.self_implements_interfaces(),
+                self.extensions(),
+                InterfaceTypeExtension::implements_interfaces,
+            )
+            .is_some()
+    }
+
     /// Get a reference to the interface definition's directives (excluding those on extensions).
     pub fn self_directives(&self) -> &[Directive] {
         self.directives.as_ref()
@@ -2242,14 +2629,32 @@ impl InterfaceTypeDefinition {
             .filter(move |directive| directive.name() == name)
     }
 
-    /// Get a reference to interface definition's fields.
-    pub fn fields_definition(&self) -> &[FieldDefinition] {
+    /// Get a reference to interface definition's fields,
+    /// excluding those from extensions.
+    pub fn self_fields(&self) -> &[FieldDefinition] {
         self.fields_definition.as_ref()
     }
 
-    /// Find a field in interface face definition.
+    /// Returns an iterator of fields of this interface type,
+    /// whether from its own definition or from extensions.
+    pub fn fields(
+        &self,
+    ) -> impl Iterator<Item = &FieldDefinition> + ExactSizeIterator + DoubleEndedIterator {
+        self.fields_by_name.iter(
+            self.self_fields(),
+            self.extensions(),
+            InterfaceTypeExtension::fields_definition,
+        )
+    }
+
+    /// Find a field by its name, either in this interface type definition or its extensions.
     pub fn field(&self, name: &str) -> Option<&FieldDefinition> {
-        self.fields_definition().iter().find(|f| f.name() == name)
+        self.fields_by_name.get(
+            name,
+            self.self_fields(),
+            self.extensions(),
+            InterfaceTypeExtension::fields_definition,
+        )
     }
 
     /// Get the AST location information for this HIR node.
@@ -2261,6 +2666,21 @@ impl InterfaceTypeDefinition {
     pub fn extensions(&self) -> &[Arc<InterfaceTypeExtension>] {
         &self.extensions
     }
+
+    pub(crate) fn push_extension(&mut self, ext: Arc<InterfaceTypeExtension>) {
+        let next_index = self.extensions.len();
+        self.fields_by_name.add_extension(
+            next_index,
+            ext.fields_definition(),
+            FieldDefinition::name,
+        );
+        self.implements_interfaces_by_name.add_extension(
+            next_index,
+            ext.implements_interfaces(),
+            ImplementsInterface::interface,
+        );
+        self.extensions.push(ext);
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -2271,6 +2691,7 @@ pub struct InputObjectTypeDefinition {
     pub(crate) input_fields_definition: Arc<Vec<InputValueDefinition>>,
     pub(crate) loc: HirNodeLocation,
     pub(crate) extensions: Vec<Arc<InputObjectTypeExtension>>,
+    pub(crate) input_fields_by_name: ByNameWithExtensions,
 }
 
 impl InputObjectTypeDefinition {
@@ -2325,11 +2746,34 @@ impl InputObjectTypeDefinition {
             .filter(move |directive| directive.name() == name)
     }
 
-    /// Get a reference to input fields definitions.
-    pub fn input_fields_definition(&self) -> &[InputValueDefinition] {
+    /// Get a reference to input fields definitions,
+    /// excluding those from extensions.
+    pub fn self_fields(&self) -> &[InputValueDefinition] {
         self.input_fields_definition.as_ref()
     }
 
+    /// Returns an iterator of fields of this input object type,
+    /// whether from its own definition or from extensions.
+    pub fn fields(
+        &self,
+    ) -> impl Iterator<Item = &InputValueDefinition> + ExactSizeIterator + DoubleEndedIterator {
+        self.input_fields_by_name.iter(
+            self.self_fields(),
+            self.extensions(),
+            InputObjectTypeExtension::input_fields_definition,
+        )
+    }
+
+    /// Find a field by its name, either in this input object type definition or its extensions.
+    pub fn field(&self, name: &str) -> Option<&InputValueDefinition> {
+        self.input_fields_by_name.get(
+            name,
+            self.self_fields(),
+            self.extensions(),
+            InputObjectTypeExtension::input_fields_definition,
+        )
+    }
+
     /// Get the AST location information for this HIR node.
     pub fn loc(&self) -> HirNodeLocation {
         self.loc
@@ -2339,6 +2783,16 @@ impl InputObjectTypeDefinition {
     pub fn extensions(&self) -> &[Arc<InputObjectTypeExtension>] {
         &self.extensions
     }
+
+    pub(crate) fn push_extension(&mut self, ext: Arc<InputObjectTypeExtension>) {
+        let next_index = self.extensions.len();
+        self.input_fields_by_name.add_extension(
+            next_index,
+            ext.input_fields_definition(),
+            InputValueDefinition::name,
+        );
+        self.extensions.push(ext);
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -2600,6 +3054,7 @@ pub struct UnionTypeExtension {
     pub(crate) directives: Arc<Vec<Directive>>,
     pub(crate) union_members: Arc<Vec<UnionMember>>,
     pub(crate) loc: HirNodeLocation,
+    pub(crate) members_by_name: ByNameWithExtensions,
 }
 
 impl UnionTypeExtension {
@@ -2838,26 +3293,187 @@ mod tests {
     #[test]
     fn extensions() {
         let mut compiler = ApolloCompiler::new();
-        let first = "
-            scalar URL
-        ";
-        let second = "
-            extend scalar URL @example(arg: true)
-        ";
+        let first = r#"
+            scalar Scalar @specifiedBy(url: "https://apollographql.com")
+            type Object implements Intf {
+                field: Int,
+            }
+            type Object2 {
+                field: String,
+            }
+            interface Intf {
+                field: Int,
+            }
+            input Input {
+                field: Enum,
+            }
+            enum Enum {
+                VALUE,
+            }
+            union Union = Object | Object2;
+        "#;
+        let second = r#"
+            extend scalar Scalar @deprecated(reason: "do something else")
+            extend interface Intf implements Intf2 {
+                field2: Scalar,
+            }
+            interface Intf2 {
+                field3: String,
+            }
+            extend type Object implements Intf2 {
+                field2: Scalar,
+                field3: String,
+            }
+            extend enum Enum {
+                "like VALUE, but more"
+                VALUE_2,
+            }
+            extend input Input {
+                field2: Int,
+            }
+            extend union Union = Query;
+            type Query {
+                object: Object,
+            }
+        "#;
         compiler.add_type_system(first, "first.graphql");
         compiler.add_type_system(second, "second.graphql");
 
-        assert!(matches!(
-            compiler
-                .db
-                .find_type_definition_by_name("URL".into())
-                .unwrap()
-                .directive_by_name("example")
+        let scalar = &compiler.db.types_definitions_by_name()["Scalar"];
+        let object = &compiler.db.object_types()["Object"];
+        let interface = &compiler.db.interfaces()["Intf"];
+        let input = &compiler.db.input_objects()["Input"];
+        let enum_ = &compiler.db.enums()["Enum"];
+        let union_ = &compiler.db.unions()["Union"];
+
+        assert_eq!(
+            scalar
+                .self_directives()
+                .iter()
+                .map(|d| d.name())
+                .collect::<Vec<_>>(),
+            ["specifiedBy"]
+        );
+        assert_eq!(
+            scalar.directives().map(|d| d.name()).collect::<Vec<_>>(),
+            ["specifiedBy", "deprecated"]
+        );
+        assert_eq!(
+            *scalar
+                .directive_by_name("deprecated")
                 .unwrap()
-                .argument_by_name("arg")
+                .argument_by_name("reason")
                 .unwrap(),
-            super::Value::Boolean(true)
-        ));
+            super::Value::String("do something else".to_owned())
+        );
+        assert!(scalar.directive_by_name("haunted").is_none());
+
+        assert_eq!(
+            object
+                .self_fields()
+                .iter()
+                .map(|f| f.name())
+                .collect::<Vec<_>>(),
+            ["field"]
+        );
+        assert_eq!(
+            object.fields().map(|f| f.name()).collect::<Vec<_>>(),
+            ["field", "field2", "field3"]
+        );
+        assert_eq!(object.field("field").unwrap().ty().name(), "Int");
+        assert!(object.field("field4").is_none());
+
+        assert_eq!(
+            object
+                .self_implements_interfaces()
+                .iter()
+                .map(|i| i.interface())
+                .collect::<Vec<_>>(),
+            ["Intf"]
+        );
+        assert_eq!(
+            object
+                .implements_interfaces()
+                .map(|f| f.interface())
+                .collect::<Vec<_>>(),
+            ["Intf", "Intf2"]
+        );
+        assert!(object.implements_interface("Intf2"));
+        assert!(!object.implements_interface("Intf3"));
+
+        assert_eq!(
+            interface
+                .self_fields()
+                .iter()
+                .map(|f| f.name())
+                .collect::<Vec<_>>(),
+            ["field"]
+        );
+        assert_eq!(
+            interface.fields().map(|f| f.name()).collect::<Vec<_>>(),
+            ["field", "field2"]
+        );
+        assert_eq!(interface.field("field").unwrap().ty().name(), "Int");
+        assert!(interface.field("field4").is_none());
+
+        assert!(interface.self_implements_interfaces().is_empty());
+        assert_eq!(
+            interface
+                .implements_interfaces()
+                .map(|f| f.interface())
+                .collect::<Vec<_>>(),
+            ["Intf2"]
+        );
+        assert!(interface.implements_interface("Intf2"));
+        assert!(!interface.implements_interface("Intf3"));
+
+        assert_eq!(
+            input
+                .self_fields()
+                .iter()
+                .map(|f| f.name())
+                .collect::<Vec<_>>(),
+            ["field"]
+        );
+        assert_eq!(
+            input.fields().map(|f| f.name()).collect::<Vec<_>>(),
+            ["field", "field2"]
+        );
+        assert_eq!(input.field("field").unwrap().ty().name(), "Enum");
+        assert!(input.field("field3").is_none());
+
+        assert_eq!(
+            enum_
+                .self_values()
+                .iter()
+                .map(|v| v.enum_value())
+                .collect::<Vec<_>>(),
+            ["VALUE"]
+        );
+        assert_eq!(
+            enum_.values().map(|v| v.enum_value()).collect::<Vec<_>>(),
+            ["VALUE", "VALUE_2"]
+        );
+        assert_eq!(
+            enum_.value("VALUE_2").unwrap().description(),
+            Some("like VALUE, but more")
+        );
+        assert!(enum_.value("VALUE_3").is_none());
+
+        assert_eq!(
+            union_
+                .self_members()
+                .iter()
+                .map(|m| m.name())
+                .collect::<Vec<_>>(),
+            ["Object", "Object2"]
+        );
+        assert_eq!(
+            union_.members().map(|m| m.name()).collect::<Vec<_>>(),
+            ["Object", "Object2", "Query"]
+        );
+        assert!(union_.has_member("Object2"));
+        assert!(!union_.has_member("Enum"));
     }
 
     #[test]
diff --git a/crates/apollo-compiler/src/database/hir_db.rs b/crates/apollo-compiler/src/database/hir_db.rs
index 5f09b47..1f48f5f 100644
--- a/crates/apollo-compiler/src/database/hir_db.rs
+++ b/crates/apollo-compiler/src/database/hir_db.rs
@@ -138,7 +138,7 @@ pub trait HirDatabase: InputDatabase + AstDatabase {
     /// Return all subscription operations in a corresponding file.
     fn subscription_operations(&self, file_id: FileId) -> Arc<Vec<Arc<OperationDefinition>>>;
 
-    /// Return all operation fields in a corresponding selection set.
+    /// Return the fields in a selection set, not including fragments.
     fn operation_fields(&self, selection_set: SelectionSet) -> Arc<Vec<Field>>;
 
     /// Return all operation inline fragment fields in a corresponding selection set.
@@ -147,6 +147,9 @@ pub trait HirDatabase: InputDatabase + AstDatabase {
     /// Return all operation fragment spread fields in a corresponding selection set.
     fn operation_fragment_spread_fields(&self, selection_set: SelectionSet) -> Arc<Vec<Field>>;
 
+    /// Return the fields that `selection_set` selects including visiting fragments and inline fragments.
+    fn flattened_operation_fields(&self, selection_set: SelectionSet) -> Vec<Arc<Field>>;
+
     /// Return all variables in a corresponding selection set.
     fn selection_variables(&self, selection_set: SelectionSet) -> Arc<HashSet<Variable>>;
 
@@ -382,7 +385,7 @@ macro_rules! by_name_extensible {
             // Orphan or incorrect extensions are reported by validation.
             if let TypeExtension::$extension_type(ext) = ext {
                 if let Some(def) = map.get_mut(ext.name()) {
-                    Arc::get_mut(def).unwrap().extensions.push(Arc::clone(ext))
+                    Arc::get_mut(def).unwrap().push_extension(Arc::clone(ext))
                 }
             }
         }
@@ -571,6 +574,9 @@ fn object_type_definition(
     let directives = directives(obj_def.directives(), file_id);
     let fields_definition = fields_definition(obj_def.fields_definition(), file_id);
     let loc = location(file_id, obj_def.syntax());
+    let fields_by_name = ByNameWithExtensions::new(&fields_definition, FieldDefinition::name);
+    let implements_interfaces_by_name =
+        ByNameWithExtensions::new(&implements_interfaces, ImplementsInterface::interface);
 
     // TODO(@goto-bus-stop) when a name is missing on this,
     // we might still want to produce a HIR node, so we can validate other parts of the definition
@@ -582,6 +588,8 @@ fn object_type_definition(
         fields_definition,
         loc,
         extensions: Vec::new(),
+        fields_by_name,
+        implements_interfaces_by_name,
     })
 }
 
@@ -643,6 +651,8 @@ fn enum_definition(
     let directives = directives(enum_def.directives(), file_id);
     let enum_values_definition = enum_values_definition(enum_def.enum_values_definition(), file_id);
     let loc = location(file_id, enum_def.syntax());
+    let values_by_name =
+        ByNameWithExtensions::new(&enum_values_definition, EnumValueDefinition::enum_value);
 
     // TODO(@goto-bus-stop) when a name is missing on this,
     // we might still want to produce a HIR node, so we can validate other parts of the definition
@@ -653,6 +663,7 @@ fn enum_definition(
         enum_values_definition,
         loc,
         extensions: Vec::new(),
+        values_by_name,
     })
 }
 
@@ -712,6 +723,7 @@ fn union_definition(
     let directives = directives(union_def.directives(), file_id);
     let union_members = union_members(union_def.union_member_types(), file_id);
     let loc = location(file_id, union_def.syntax());
+    let members_by_name = ByNameWithExtensions::new(&union_members, UnionMember::name);
 
     // TODO(@goto-bus-stop) when a name is missing on this,
     // we might still want to produce a HIR node, so we can validate other parts of the definition
@@ -722,6 +734,7 @@ fn union_definition(
         union_members,
         loc,
         extensions: Vec::new(),
+        members_by_name,
     })
 }
 
@@ -730,11 +743,17 @@ fn union_extension(
     def: ast::UnionTypeExtension,
     file_id: FileId,
 ) -> Option<Arc<UnionTypeExtension>> {
+    let directives = directives(def.directives(), file_id);
+    let name = name(def.name(), file_id)?;
+    let union_members = union_members(def.union_member_types(), file_id);
+    let loc = location(file_id, def.syntax());
+    let members_by_name = ByNameWithExtensions::new(&union_members, UnionMember::name);
     Some(Arc::new(UnionTypeExtension {
-        directives: directives(def.directives(), file_id),
-        name: name(def.name(), file_id)?,
-        union_members: union_members(def.union_member_types(), file_id),
-        loc: location(file_id, def.syntax()),
+        directives,
+        name,
+        union_members,
+        loc,
+        members_by_name,
     }))
 }
 
@@ -773,6 +792,9 @@ fn interface_definition(
     let directives = directives(interface_def.directives(), file_id);
     let fields_definition = fields_definition(interface_def.fields_definition(), file_id);
     let loc = location(file_id, interface_def.syntax());
+    let fields_by_name = ByNameWithExtensions::new(&fields_definition, FieldDefinition::name);
+    let implements_interfaces_by_name =
+        ByNameWithExtensions::new(&implements_interfaces, ImplementsInterface::interface);
 
     // TODO(@goto-bus-stop) when a name is missing on this,
     // we might still want to produce a HIR node, so we can validate other parts of the definition
@@ -784,6 +806,8 @@ fn interface_definition(
         fields_definition,
         loc,
         extensions: Vec::new(),
+        fields_by_name,
+        implements_interfaces_by_name,
     })
 }
 
@@ -836,6 +860,8 @@ fn input_object_definition(
     let input_fields_definition =
         input_fields_definition(input_obj.input_fields_definition(), file_id);
     let loc = location(file_id, input_obj.syntax());
+    let input_fields_by_name =
+        ByNameWithExtensions::new(&input_fields_definition, InputValueDefinition::name);
 
     // TODO(@goto-bus-stop) when a name is missing on this,
     // we might still want to produce a HIR node, so we can validate other parts of the definition
@@ -846,6 +872,7 @@ fn input_object_definition(
         input_fields_definition,
         loc,
         extensions: Vec::new(),
+        input_fields_by_name,
     })
 }
 
diff --git a/crates/apollo-compiler/src/database/inputs.rs b/crates/apollo-compiler/src/database/inputs.rs
index 26962f5..8cc19e3 100644
--- a/crates/apollo-compiler/src/database/inputs.rs
+++ b/crates/apollo-compiler/src/database/inputs.rs
@@ -14,7 +14,7 @@ struct UnknownFileError;
 /// Use [`InputDatabase::source_cache`] to construct one.
 #[derive(Clone, PartialEq, Eq)]
 pub struct SourceCache {
-    sources: Arc<HashMap<FileId, Arc<AriadneSource>>>,
+    sources: HashMap<FileId, Arc<AriadneSource>>,
     paths: HashMap<FileId, PathBuf>,
 }
 impl AriadneCache<FileId> for &SourceCache {
@@ -78,7 +78,7 @@ pub trait InputDatabase {
     fn source_with_lines(&self, file_id: FileId) -> Arc<AriadneSource>;
     /// Get all GraphQL sources known to the compiler, split up into lines
     /// for printing diagnostics.
-    fn source_cache(&self) -> SourceCache;
+    fn source_cache(&self) -> Arc<SourceCache>;
 
     /// Get all type system definition (GraphQL schema) files.
     fn type_definition_files(&self) -> Vec<FileId>;
@@ -106,7 +106,7 @@ fn source_with_lines(db: &dyn InputDatabase, file_id: FileId) -> Arc<AriadneSour
     Arc::new(AriadneSource::from(code))
 }
 
-fn source_cache(db: &dyn InputDatabase) -> SourceCache {
+fn source_cache(db: &dyn InputDatabase) -> Arc<SourceCache> {
     let file_ids = db.source_files();
     let sources = file_ids
         .iter()
@@ -116,10 +116,7 @@ fn source_cache(db: &dyn InputDatabase) -> SourceCache {
         .iter()
         .map(|&id| (id, db.input(id).filename().to_owned()))
         .collect();
-    SourceCache {
-        sources: Arc::new(sources),
-        paths,
-    }
+    Arc::new(SourceCache { sources, paths })
 }
 
 fn type_definition_files(db: &dyn InputDatabase) -> Vec<FileId> {
diff --git a/crates/apollo-compiler/src/diagnostics.rs b/crates/apollo-compiler/src/diagnostics.rs
index 0329521..0b2d9b0 100644
--- a/crates/apollo-compiler/src/diagnostics.rs
+++ b/crates/apollo-compiler/src/diagnostics.rs
@@ -1,4 +1,4 @@
-use std::fmt;
+use std::{fmt, sync::Arc};
 
 use crate::database::hir::{DirectiveLocation, HirNodeLocation};
 use crate::database::{InputDatabase, SourceCache};
@@ -83,11 +83,11 @@ impl Label {
 
 #[derive(Debug, Error, Clone, PartialEq, Eq)]
 pub struct ApolloDiagnostic {
-    cache: SourceCache,
+    cache: Arc<SourceCache>,
     pub location: DiagnosticLocation,
     pub labels: Vec<Label>,
     pub help: Option<String>,
-    pub data: DiagnosticData,
+    pub data: Box<DiagnosticData>,
 }
 
 impl ApolloDiagnostic {
@@ -101,7 +101,7 @@ impl ApolloDiagnostic {
             location,
             labels: vec![],
             help: None,
-            data,
+            data: Box::new(data),
         }
     }
 
@@ -128,7 +128,9 @@ impl ApolloDiagnostic {
 impl fmt::Display for ApolloDiagnostic {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         let mut buf = std::io::Cursor::new(Vec::<u8>::new());
-        self.to_report().write(&self.cache, &mut buf).unwrap();
+        self.to_report()
+            .write(self.cache.as_ref(), &mut buf)
+            .unwrap();
         writeln!(f, "{}", std::str::from_utf8(&buf.into_inner()).unwrap())
     }
 }
@@ -137,7 +139,7 @@ impl fmt::Display for ApolloDiagnostic {
 #[derive(Debug, Error, Clone, Hash, PartialEq, Eq)]
 #[non_exhaustive]
 pub enum DiagnosticData {
-    #[error("syntax error")]
+    #[error("syntax error: {message}")]
     SyntaxError { message: String },
     #[error("expected identifier")]
     MissingIdent,
@@ -269,6 +271,13 @@ pub enum DiagnosticData {
     DisallowedSubselection,
     #[error("interface, union and object types must have a subselection set")]
     MissingSubselection,
+    #[error("operation must not select different types using the same field name `{field}`")]
+    ConflictingField {
+        /// Name of the non-unique field.
+        field: String,
+        original_selection: DiagnosticLocation,
+        redefined_selection: DiagnosticLocation,
+    },
 }
 
 impl DiagnosticData {
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index e0b01b8..915dda0 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -748,7 +748,7 @@ enum Pet {
 
         let enums = compiler.db.enums();
         let enum_values: Vec<&str> = enums["Pet"]
-            .enum_values_definition()
+            .self_values()
             .iter()
             .map(|enum_val| enum_val.enum_value())
             .collect();
@@ -790,14 +790,14 @@ type SearchQuery {
 
         let unions = compiler.db.unions();
         let union_members: Vec<&str> = unions["SearchResult"]
-            .union_members()
+            .self_members()
             .iter()
             .map(|member| member.name())
             .collect();
         assert_eq!(union_members, ["Photo", "Person"]);
 
         let photo_object = unions["SearchResult"]
-            .union_members()
+            .self_members()
             .iter()
             .find(|mem| mem.name() == "Person")
             .unwrap()
@@ -805,7 +805,7 @@ type SearchQuery {
 
         if let Some(photo) = photo_object {
             let fields: Vec<&str> = photo
-                .fields_definition()
+                .self_fields()
                 .iter()
                 .map(|field| field.name())
                 .collect();
@@ -873,7 +873,7 @@ input Point2D {
 
         let input_objects = compiler.db.input_objects();
         let fields: Vec<&str> = input_objects["Point2D"]
-            .input_fields_definition()
+            .self_fields()
             .iter()
             .map(|val| val.name())
             .collect();
@@ -944,7 +944,7 @@ scalar Url @specifiedBy(url: "https://tools.ietf.org/html/rfc3986")
 
         if let Some(person) = person_obj {
             let field_ty_directive: Vec<String> = person
-                .fields_definition()
+                .self_fields()
                 .iter()
                 .filter_map(|f| {
                     // get access to the actual definition the field is using
@@ -969,7 +969,7 @@ scalar Url @specifiedBy(url: "https://tools.ietf.org/html/rfc3986")
             assert_eq!(field_ty_directive, ["specifiedBy"]);
 
             let field_arg_ty_vals: Vec<String> = person
-                .fields_definition()
+                .self_fields()
                 .iter()
                 .flat_map(|f| {
                     let enum_vals: Vec<String> = f
@@ -982,7 +982,7 @@ scalar Url @specifiedBy(url: "https://tools.ietf.org/html/rfc3986")
                                     // get that definition's directives, for example
                                     TypeDefinition::EnumTypeDefinition(enum_) => {
                                         let dir_names: Vec<String> = enum_
-                                            .enum_values_definition()
+                                            .self_values()
                                             .iter()
                                             .map(|enum_val| enum_val.enum_value().to_owned())
                                             .collect();
@@ -1026,7 +1026,7 @@ scalar Url @specifiedBy(url: "https://tools.ietf.org/html/rfc3986")
 
         if let Some(person) = person_obj {
             let field_ty_directive: Vec<String> = person
-                .input_fields_definition()
+                .self_fields()
                 .iter()
                 .filter_map(|f| {
                     if let Some(field_ty) = f.ty().type_def(&compiler.db) {
diff --git a/crates/apollo-compiler/src/validation/enum_.rs b/crates/apollo-compiler/src/validation/enum_.rs
index a5a2104..c62d6ae 100644
--- a/crates/apollo-compiler/src/validation/enum_.rs
+++ b/crates/apollo-compiler/src/validation/enum_.rs
@@ -27,7 +27,7 @@ pub fn validate_enum_definition(
     );
 
     let mut seen: HashMap<&str, &EnumValueDefinition> = HashMap::new();
-    for enum_val in enum_def.enum_values_definition() {
+    for enum_val in enum_def.self_values() {
         diagnostics.extend(db.validate_enum_value(enum_val.clone()));
 
         // An Enum type must define one or more unique enum values.
diff --git a/crates/apollo-compiler/src/validation/field.rs b/crates/apollo-compiler/src/validation/field.rs
index 2159d17..2772405 100644
--- a/crates/apollo-compiler/src/validation/field.rs
+++ b/crates/apollo-compiler/src/validation/field.rs
@@ -220,8 +220,6 @@ pub fn validate_field_definitions(
     diagnostics
 }
 
-// TODO(@lrlna): don't allow this!!
-#[allow(clippy::result_large_err)]
 pub fn validate_leaf_field_selection(
     db: &dyn ValidationDatabase,
     field: Arc<hir::Field>,
diff --git a/crates/apollo-compiler/src/validation/interface.rs b/crates/apollo-compiler/src/validation/interface.rs
index cd94f45..5318693 100644
--- a/crates/apollo-compiler/src/validation/interface.rs
+++ b/crates/apollo-compiler/src/validation/interface.rs
@@ -44,7 +44,7 @@ pub fn validate_interface_definition(
     //   name: String
     // }
     for (name, interface_def) in db.interfaces().iter() {
-        for implements_interface in interface_def.implements_interfaces() {
+        for implements_interface in interface_def.self_implements_interfaces() {
             if let Some(interface) = implements_interface.interface_definition(db.upcast()) {
                 let super_name = interface.name();
                 if name == super_name {
@@ -67,11 +67,12 @@ pub fn validate_interface_definition(
     }
 
     // Interface Type field validation.
-    diagnostics.extend(db.validate_field_definitions(interface_def.fields_definition().to_vec()));
+    diagnostics.extend(db.validate_field_definitions(interface_def.self_fields().to_vec()));
 
     // Implements Interfaceds validation.
-    diagnostics
-        .extend(db.validate_implements_interfaces(interface_def.implements_interfaces().to_vec()));
+    diagnostics.extend(
+        db.validate_implements_interfaces(interface_def.self_implements_interfaces().to_vec()),
+    );
 
     // When defining an interface that implements another interface, the
     // implementing interface must define each field that is specified by
@@ -79,17 +80,17 @@ pub fn validate_interface_definition(
     //
     // Returns a Missing Field error.
     let fields: HashSet<ValidationSet> = interface_def
-        .fields_definition()
+        .self_fields()
         .iter()
         .map(|field| ValidationSet {
             name: field.name().into(),
             loc: field.loc(),
         })
         .collect();
-    for implements_interface in interface_def.implements_interfaces().iter() {
+    for implements_interface in interface_def.self_implements_interfaces().iter() {
         if let Some(super_interface) = implements_interface.interface_definition(db.upcast()) {
             let implements_interface_fields: HashSet<ValidationSet> = super_interface
-                .fields_definition()
+                .self_fields()
                 .iter()
                 .map(|field| ValidationSet {
                     name: field.name().into(),
@@ -176,7 +177,7 @@ pub fn validate_implements_interfaces(
         .filter_map(|implements_interface| {
             if let Some(interface) = implements_interface.interface_definition(db.upcast()) {
                 let child_interfaces: HashSet<ValidationSet> = interface
-                    .implements_interfaces()
+                    .self_implements_interfaces()
                     .iter()
                     .map(|interface| ValidationSet {
                         name: interface.interface().to_owned(),
diff --git a/crates/apollo-compiler/src/validation/object.rs b/crates/apollo-compiler/src/validation/object.rs
index f1da414..685c470 100644
--- a/crates/apollo-compiler/src/validation/object.rs
+++ b/crates/apollo-compiler/src/validation/object.rs
@@ -30,10 +30,11 @@ pub fn validate_object_type_definition(
     ));
 
     // Object Type field validations.
-    diagnostics.extend(db.validate_field_definitions(object.fields_definition().to_vec()));
+    diagnostics.extend(db.validate_field_definitions(object.self_fields().to_vec()));
 
     // Implements Interfaces validation.
-    diagnostics.extend(db.validate_implements_interfaces(object.implements_interfaces().to_vec()));
+    diagnostics
+        .extend(db.validate_implements_interfaces(object.self_implements_interfaces().to_vec()));
 
     // When defining an interface that implements another interface, the
     // implementing interface must define each field that is specified by
@@ -41,17 +42,17 @@ pub fn validate_object_type_definition(
     //
     // Returns a Missing Field error.
     let fields: HashSet<ValidationSet> = object
-        .fields_definition()
+        .self_fields()
         .iter()
         .map(|field| ValidationSet {
             name: field.name().into(),
             loc: field.loc(),
         })
         .collect();
-    for implements_interface in object.implements_interfaces().iter() {
+    for implements_interface in object.self_implements_interfaces().iter() {
         if let Some(interface) = implements_interface.interface_definition(db.upcast()) {
             let implements_interface_fields: HashSet<ValidationSet> = interface
-                .fields_definition()
+                .self_fields()
                 .iter()
                 .map(|field| ValidationSet {
                     name: field.name().into(),
diff --git a/crates/apollo-compiler/src/validation/selection.rs b/crates/apollo-compiler/src/validation/selection.rs
index 36d5233..8701577 100644
--- a/crates/apollo-compiler/src/validation/selection.rs
+++ b/crates/apollo-compiler/src/validation/selection.rs
@@ -1,6 +1,305 @@
+use indexmap::IndexMap;
 use std::sync::Arc;
 
-use crate::{hir, validation::ValidationDatabase, ApolloDiagnostic};
+use crate::{
+    diagnostics::{ApolloDiagnostic, DiagnosticData, Label},
+    hir,
+    validation::ValidationDatabase,
+};
+
+/// Check if two fields will output the same type.
+///
+/// Spec: https://spec.graphql.org/October2021/#SameResponseShape()
+pub(crate) fn same_response_shape(
+    db: &dyn ValidationDatabase,
+    field_a: Arc<hir::Field>,
+    field_b: Arc<hir::Field>,
+) -> Result<(), ApolloDiagnostic> {
+    // 1. Let typeA be the return type of fieldA.
+    let Some(full_type_a) = field_a.ty(db.upcast()) else {
+        return Ok(()); // Can't do much if we don't know the type
+    };
+    // 2. Let typeB be the return type of fieldB.
+    let Some(full_type_b) = field_b.ty(db.upcast()) else {
+        return Ok(()); // Can't do much if we don't know the type
+    };
+
+    let mut type_a = &full_type_a;
+    let mut type_b = &full_type_b;
+
+    let mismatching_type_diagnostic = || {
+        ApolloDiagnostic::new(
+            db,
+            field_b.loc().into(),
+            DiagnosticData::ConflictingField {
+                field: field_a.name().to_string(),
+                original_selection: field_a.loc().into(),
+                redefined_selection: field_b.loc().into(),
+            },
+        )
+        .label(Label::new(
+            field_a.loc(),
+            format!(
+                "`{}` has type `{}` here",
+                field_a.response_name(),
+                full_type_a
+            ),
+        ))
+        .label(Label::new(
+            field_b.loc(),
+            format!("but the same field name has type `{full_type_b}` here"),
+        ))
+    };
+
+    while !type_a.is_named() || !type_b.is_named() {
+        // 3. If typeA or typeB is Non-Null.
+        // 3a. If typeA or typeB is nullable, return false.
+        // 3b. Let typeA be the nullable type of typeA
+        // 3c. Let typeB be the nullable type of typeB
+        (type_a, type_b) = match (type_a, type_b) {
+            (hir::Type::NonNull { ty: type_a, .. }, hir::Type::NonNull { ty: type_b, .. }) => {
+                (type_a.as_ref(), type_b.as_ref())
+            }
+            (hir::Type::NonNull { .. }, _) | (_, hir::Type::NonNull { .. }) => {
+                return Err(mismatching_type_diagnostic())
+            }
+            (type_a, type_b) => (type_a, type_b),
+        };
+
+        // 4. If typeA or typeB is List.
+        // 4a. If typeA or typeB is not List, return false.
+        // 4b. Let typeA be the item type of typeA
+        // 4c. Let typeB be the item type of typeB
+        (type_a, type_b) = match (type_a, type_b) {
+            (hir::Type::List { ty: type_a, .. }, hir::Type::List { ty: type_b, .. }) => {
+                (type_a.as_ref(), type_b.as_ref())
+            }
+            (hir::Type::List { .. }, _) | (_, hir::Type::List { .. }) => {
+                return Err(mismatching_type_diagnostic())
+            }
+            (type_a, type_b) => (type_a, type_b),
+        };
+
+        // 4d. Repeat from step 3.
+    }
+
+    let (Some(def_a), Some(def_b)) = (type_a.type_def(db.upcast()), type_b.type_def(db.upcast())) else {
+        return Ok(()); // Cannot do much if we don't know the type
+    };
+
+    match (def_a, def_b) {
+        // 5. If typeA or typeB is Scalar or Enum.
+        (
+            def_a @ (hir::TypeDefinition::ScalarTypeDefinition(_)
+            | hir::TypeDefinition::EnumTypeDefinition(_)),
+            def_b @ (hir::TypeDefinition::ScalarTypeDefinition(_)
+            | hir::TypeDefinition::EnumTypeDefinition(_)),
+        ) => {
+            // 5a. If typeA and typeB are the same type return true, otherwise return false.
+            if def_a == def_b {
+                Ok(())
+            } else {
+                Err(mismatching_type_diagnostic())
+            }
+        }
+        // 6. Assert: typeA and typeB are both composite types.
+        (def_a, def_b) if def_a.is_composite_definition() && def_b.is_composite_definition() => {
+            let merged_set = field_a.selection_set().merge(field_b.selection_set());
+            let fields = db.flattened_operation_fields(merged_set);
+            let grouped_by_name = group_fields_by_name(fields);
+
+            for (_, fields_for_name) in grouped_by_name {
+                // 9. Given each pair of members subfieldA and subfieldB in fieldsForName:
+                let Some((subfield_a, rest)) = fields_for_name.split_first() else {
+                    continue;
+                };
+                for subfield_b in rest {
+                    // 9a. If SameResponseShape(subfieldA, subfieldB) is false, return false.
+                    db.same_response_shape(Arc::clone(subfield_a), Arc::clone(subfield_b))?;
+                }
+            }
+
+            Ok(())
+        }
+        (_, _) => Ok(()),
+    }
+}
+
+/// Given a list of fields, group them by response name.
+fn group_fields_by_name(fields: Vec<Arc<hir::Field>>) -> IndexMap<String, Vec<Arc<hir::Field>>> {
+    let mut map = IndexMap::<String, Vec<Arc<hir::Field>>>::new();
+    for field in fields {
+        match map.entry(field.response_name().to_string()) {
+            indexmap::map::Entry::Occupied(mut entry) => {
+                entry.get_mut().push(field);
+            }
+            indexmap::map::Entry::Vacant(entry) => {
+                entry.insert(vec![field]);
+            }
+        }
+    }
+    map
+}
+
+/// Check if the arguments provided to two fields are the same, so the fields can be merged.
+fn identical_arguments(
+    db: &dyn ValidationDatabase,
+    field_a: &hir::Field,
+    field_b: &hir::Field,
+) -> Result<(), ApolloDiagnostic> {
+    let args_a = field_a.arguments();
+    let args_b = field_b.arguments();
+    // Check if fieldB provides the same argument names and values as fieldA (order-independent).
+    for arg in args_a {
+        let Some(other_arg) = args_b.iter().find(|other_arg| other_arg.name() == arg.name()) else {
+            return Err(
+                ApolloDiagnostic::new(
+                    db,
+                    field_b.loc().into(),
+                    DiagnosticData::ConflictingField {
+                        field: field_a.name().to_string(),
+                        original_selection: field_a.loc().into(),
+                        redefined_selection: field_b.loc().into(),
+                    },
+                )
+                .label(Label::new(arg.loc(), format!("field `{}` is selected with argument `{}` here", field_a.name(), arg.name())))
+                .label(Label::new(field_b.loc(), format!("but argument `{}` is not provided here", arg.name())))
+                .help("Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.")
+            );
+        };
+
+        if !other_arg.value.is_same_value(&arg.value) {
+            return Err(
+                ApolloDiagnostic::new(
+                    db,
+                    field_b.loc().into(),
+                    DiagnosticData::ConflictingField {
+                        field: field_a.name().to_string(),
+                        original_selection: field_a.loc().into(),
+                        redefined_selection: field_b.loc().into(),
+                    },
+                )
+                .label(Label::new(arg.loc(), format!("field `{}` provides one argument value here", field_a.name())))
+                .label(Label::new(other_arg.loc(), "but a different value here"))
+                .help("Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.")
+            );
+        }
+    }
+    // Check if fieldB provides any arguments that fieldA does not provide.
+    for arg in args_b {
+        if !args_a
+            .iter()
+            .any(|other_arg| other_arg.name() == arg.name())
+        {
+            return Err(
+                ApolloDiagnostic::new(
+                    db,
+                    field_b.loc().into(),
+                    DiagnosticData::ConflictingField {
+                        field: field_a.name().to_string(),
+                        original_selection: field_a.loc().into(),
+                        redefined_selection: field_b.loc().into(),
+                    },
+                )
+                .label(Label::new(arg.loc(), format!("field `{}` is selected with argument `{}` here", field_b.name(), arg.name())))
+                .label(Label::new(field_a.loc(), format!("but argument `{}` is not provided here", arg.name())))
+                .help("Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.")
+            );
+        };
+    }
+
+    Ok(())
+}
+
+/// Check if the fields in a given selection set can be merged.
+///
+/// Spec: https://spec.graphql.org/October2021/#FieldsInSetCanMerge()
+pub(crate) fn fields_in_set_can_merge(
+    db: &dyn ValidationDatabase,
+    selection_set: hir::SelectionSet,
+) -> Result<(), Vec<ApolloDiagnostic>> {
+    // 1. Let `fieldsForName` be the set of selections with a given response name in set including visiting fragments and inline fragments.
+    let fields = db.flattened_operation_fields(selection_set);
+    let grouped_by_name = group_fields_by_name(fields);
+
+    let mut diagnostics = vec![];
+
+    for (_, fields_for_name) in grouped_by_name {
+        let Some((field_a, rest)) = fields_for_name.split_first() else {
+            continue; // Nothing to merge
+        };
+        let Some(parent_a) = field_a.parent_type(db.upcast()) else {
+            continue; // We can't find the type
+        };
+
+        // 2. Given each pair of members fieldA and fieldB in fieldsForName:
+        for field_b in rest {
+            // 2a. SameResponseShape(fieldA, fieldB) must be true.
+            if let Err(diagnostic) =
+                db.same_response_shape(Arc::clone(field_a), Arc::clone(field_b))
+            {
+                diagnostics.push(diagnostic);
+                continue;
+            }
+            // 2b. If the parent types of fieldA and fieldB are equal or if either is not an Object Type:
+            let Some(parent_b) = field_b.parent_type(db.upcast()) else {
+                continue; // We can't find the type
+            };
+            if parent_a == parent_b {
+                // 2bi. fieldA and fieldB must have identical field names.
+                if field_a.name() != field_b.name() {
+                    diagnostics.push(
+                        ApolloDiagnostic::new(
+                            db,
+                            field_b.loc().into(),
+                            DiagnosticData::ConflictingField {
+                                field: field_b.name().to_string(),
+                                original_selection: field_a.loc().into(),
+                                redefined_selection: field_b.loc().into(),
+                            },
+                        )
+                        .label(Label::new(
+                            field_a.loc(),
+                            format!(
+                                "field `{}` is selected from field `{}` here",
+                                field_a.response_name(),
+                                field_a.name()
+                            ),
+                        ))
+                        .label(Label::new(
+                            field_b.loc(),
+                            format!(
+                                "but the same field `{}` is also selected from field `{}` here",
+                                field_b.response_name(),
+                                field_b.name()
+                            ),
+                        ))
+                        .help("Alias is already used for a different field"),
+                    );
+                    continue;
+                }
+                // 2bii. fieldA and fieldB must have identical sets of arguments.
+                if let Err(diagnostic) = identical_arguments(db, field_a, field_b) {
+                    diagnostics.push(diagnostic);
+                    continue;
+                }
+                // 2biii. Let mergedSet be the result of adding the selection set of fieldA and the selection set of fieldB.
+                let merged_set = field_a.selection_set().merge(field_b.selection_set());
+                // 2biv. FieldsInSetCanMerge(mergedSet) must be true.
+                if let Err(sub_diagnostics) = db.fields_in_set_can_merge(merged_set) {
+                    diagnostics.extend(sub_diagnostics);
+                    continue;
+                }
+            }
+        }
+    }
+
+    if diagnostics.is_empty() {
+        Ok(())
+    } else {
+        Err(diagnostics)
+    }
+}
 
 pub fn validate_selection(
     db: &dyn ValidationDatabase,
@@ -35,6 +334,10 @@ pub fn validate_selection_set(
 ) -> Vec<ApolloDiagnostic> {
     let mut diagnostics = Vec::new();
 
+    if let Err(diagnostic) = db.fields_in_set_can_merge(selection_set.clone()) {
+        diagnostics.extend(diagnostic);
+    }
+
     diagnostics.extend(db.validate_selection(selection_set.selection));
 
     diagnostics
diff --git a/crates/apollo-compiler/src/validation/union_.rs b/crates/apollo-compiler/src/validation/union_.rs
index b5dfacc..40ebcbe 100644
--- a/crates/apollo-compiler/src/validation/union_.rs
+++ b/crates/apollo-compiler/src/validation/union_.rs
@@ -27,7 +27,7 @@ pub fn validate_union_definition(
     );
 
     let mut seen: HashMap<&str, &UnionMember> = HashMap::new();
-    for union_member in union_def.union_members().iter() {
+    for union_member in union_def.self_members().iter() {
         let name = union_member.name();
         let redefined_definition = union_member.loc();
         // A Union type must include one or more unique member types.
diff --git a/crates/apollo-compiler/src/validation/validation_db.rs b/crates/apollo-compiler/src/validation/validation_db.rs
index b076c8c..08393f3 100644
--- a/crates/apollo-compiler/src/validation/validation_db.rs
+++ b/crates/apollo-compiler/src/validation/validation_db.rs
@@ -180,6 +180,34 @@ pub trait ValidationDatabase:
 
     #[salsa::invoke(variable::validate_unused_variables)]
     fn validate_unused_variable(&self, op: Arc<OperationDefinition>) -> Vec<ApolloDiagnostic>;
+
+    /// Check if two fields will output the same type.
+    ///
+    /// If the two fields output different types, returns an `Err` containing diagnostic information.
+    /// To simply check if outputs are the same, you can use `.is_ok()`:
+    /// ```rust,ignore
+    /// let is_same = db.same_response_shape(field_a, field_b).is_ok();
+    /// // `is_same` is `bool`
+    /// ```
+    ///
+    /// Spec: https://spec.graphql.org/October2021/#SameResponseShape()
+    #[salsa::invoke(selection::same_response_shape)]
+    fn same_response_shape(
+        &self,
+        field_a: Arc<Field>,
+        field_b: Arc<Field>,
+    ) -> Result<(), ApolloDiagnostic>;
+
+    /// Check if the fields in a given selection set can be merged.
+    ///
+    /// If the fields cannot be merged, returns an `Err` containing diagnostic information.
+    ///
+    /// Spec: https://spec.graphql.org/October2021/#FieldsInSetCanMerge()
+    #[salsa::invoke(selection::fields_in_set_can_merge)]
+    fn fields_in_set_can_merge(
+        &self,
+        selection_set: SelectionSet,
+    ) -> Result<(), Vec<ApolloDiagnostic>>;
 }
 
 pub fn validate(db: &dyn ValidationDatabase) -> Vec<ApolloDiagnostic> {


Test patch:
diff --git a/crates/apollo-compiler/src/tests.rs b/crates/apollo-compiler/src/tests.rs
index fb93ec8..dd4f1b8 100644
--- a/crates/apollo-compiler/src/tests.rs
+++ b/crates/apollo-compiler/src/tests.rs
@@ -58,8 +58,9 @@ fn assert_diagnostics_are_present(errors: &[ApolloDiagnostic], path: &Path) {
 
 fn assert_diagnostics_are_absent(errors: &[ApolloDiagnostic], path: &Path) {
     if !errors.is_empty() {
-        let formatted: Vec<String> = errors.iter().map(|e| format!("{e:?}")).collect();
-        println!("{:?}", formatted.join("\n"));
+        for diagnostic in errors {
+            println!("{diagnostic}");
+        }
         panic!(
             "There should be no diagnostics in the file {:?}",
             path.display(),
diff --git a/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.graphql b/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.graphql
index ca4a976..352aacf 100644
--- a/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.graphql
+++ b/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.graphql
@@ -12,6 +12,8 @@ type Query {
 
 fragment Y on ComplicatedArgs {
     multipleReqs
+}
+fragment Z on ComplicatedArgs {
     multipleReqs(req2: 1)
 }
 
@@ -21,8 +23,8 @@ fragment Y on ComplicatedArgs {
   }
 
   complicatedArgs {
-    multipleReqs(req2: 1)
-    multipleOptAndReq(req1: 1)
-    multipleOptAndReq(opt1: 1, opt2: 1, req1: 1)
+    a: multipleReqs(req2: 1)
+    b: multipleOptAndReq(req1: 1)
+    c: multipleOptAndReq(opt1: 1, opt2: 1, req1: 1)
   }
 }
diff --git a/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt b/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt
index a2ea8a0..f960f9b 100644
--- a/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt
+++ b/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt
@@ -7,7 +7,7 @@
             file_id: FileId {
                 id: 53,
             },
-            offset: 333,
+            offset: 367,
             length: 5,
         },
         labels: [
@@ -16,7 +16,7 @@
                     file_id: FileId {
                         id: 53,
                     },
-                    offset: 333,
+                    offset: 367,
                     length: 5,
                 },
                 text: "missing value for argument `if`",
@@ -35,7 +35,7 @@
             file_id: FileId {
                 id: 53,
             },
-            offset: 348,
+            offset: 382,
             length: 12,
         },
         labels: [
@@ -44,7 +44,7 @@
                     file_id: FileId {
                         id: 53,
                     },
-                    offset: 348,
+                    offset: 382,
                     length: 12,
                 },
                 text: "argument by this name not found",
@@ -63,7 +63,7 @@
             file_id: FileId {
                 id: 53,
             },
-            offset: 339,
+            offset: 373,
             length: 22,
         },
         labels: [
@@ -72,7 +72,7 @@
                     file_id: FileId {
                         id: 53,
                     },
-                    offset: 339,
+                    offset: 373,
                     length: 22,
                 },
                 text: "missing value for argument `if`",
@@ -91,8 +91,8 @@
             file_id: FileId {
                 id: 53,
             },
-            offset: 399,
-            length: 21,
+            offset: 433,
+            length: 24,
         },
         labels: [
             Label {
@@ -100,8 +100,8 @@
                     file_id: FileId {
                         id: 53,
                     },
-                    offset: 399,
-                    length: 21,
+                    offset: 433,
+                    length: 24,
                 },
                 text: "missing value for argument `req1`",
             },
@@ -129,8 +129,8 @@
             file_id: FileId {
                 id: 53,
             },
-            offset: 425,
-            length: 26,
+            offset: 462,
+            length: 29,
         },
         labels: [
             Label {
@@ -138,8 +138,8 @@
                     file_id: FileId {
                         id: 53,
                     },
-                    offset: 425,
-                    length: 26,
+                    offset: 462,
+                    length: 29,
                 },
                 text: "missing value for argument `req2`",
             },
@@ -167,8 +167,8 @@
             file_id: FileId {
                 id: 53,
             },
-            offset: 456,
-            length: 44,
+            offset: 496,
+            length: 47,
         },
         labels: [
             Label {
@@ -176,8 +176,8 @@
                     file_id: FileId {
                         id: 53,
                     },
-                    offset: 456,
-                    length: 44,
+                    offset: 496,
+                    length: 47,
                 },
                 text: "missing value for argument `req2`",
             },
@@ -281,7 +281,7 @@
             file_id: FileId {
                 id: 53,
             },
-            offset: 298,
+            offset: 332,
             length: 21,
         },
         labels: [
@@ -290,7 +290,7 @@
                     file_id: FileId {
                         id: 53,
                     },
-                    offset: 298,
+                    offset: 332,
                     length: 21,
                 },
                 text: "missing value for argument `req1`",
diff --git a/crates/apollo-compiler/test_data/diagnostics/0074_merge_identical_fields.graphql b/crates/apollo-compiler/test_data/diagnostics/0074_merge_identical_fields.graphql
new file mode 100644
index 0000000..2dd1f58
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0074_merge_identical_fields.graphql
@@ -0,0 +1,14 @@
+type Dog {
+  nickname: String
+  name: String
+}
+
+fragment conflictingBecauseAlias on Dog {
+  name: nickname
+  name
+}
+
+fragment sameAliasesWithDifferentFieldTargets on Dog {
+  fido: name
+  fido: nickname
+}
diff --git a/crates/apollo-compiler/test_data/diagnostics/0074_merge_identical_fields.txt b/crates/apollo-compiler/test_data/diagnostics/0074_merge_identical_fields.txt
new file mode 100644
index 0000000..295ce3d
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0074_merge_identical_fields.txt
@@ -0,0 +1,110 @@
+[
+    ApolloDiagnostic {
+        cache: {
+            70: "0074_merge_identical_fields.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 70,
+            },
+            offset: 109,
+            length: 4,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 70,
+                    },
+                    offset: 92,
+                    length: 14,
+                },
+                text: "field `name` is selected from field `nickname` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 70,
+                    },
+                    offset: 109,
+                    length: 4,
+                },
+                text: "but the same field `name` is also selected from field `name` here",
+            },
+        ],
+        help: Some(
+            "Alias is already used for a different field",
+        ),
+        data: ConflictingField {
+            field: "name",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 70,
+                },
+                offset: 92,
+                length: 14,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 70,
+                },
+                offset: 109,
+                length: 4,
+            },
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            70: "0074_merge_identical_fields.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 70,
+            },
+            offset: 187,
+            length: 14,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 70,
+                    },
+                    offset: 174,
+                    length: 10,
+                },
+                text: "field `fido` is selected from field `name` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 70,
+                    },
+                    offset: 187,
+                    length: 14,
+                },
+                text: "but the same field `fido` is also selected from field `nickname` here",
+            },
+        ],
+        help: Some(
+            "Alias is already used for a different field",
+        ),
+        data: ConflictingField {
+            field: "nickname",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 70,
+                },
+                offset: 174,
+                length: 10,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 70,
+                },
+                offset: 187,
+                length: 14,
+            },
+        },
+    },
+]
diff --git a/crates/apollo-compiler/test_data/diagnostics/0075_merge_conflicting_args.graphql b/crates/apollo-compiler/test_data/diagnostics/0075_merge_conflicting_args.graphql
new file mode 100644
index 0000000..6d2ab4a
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0075_merge_conflicting_args.graphql
@@ -0,0 +1,34 @@
+enum DogCommand {
+  SIT
+  HEEL
+}
+
+type Dog {
+  doesKnowCommand(dogCommand: DogCommand): Boolean
+  isAtLocation(x: Int, y: Int): Boolean
+}
+
+fragment conflictingArgsOnValues on Dog {
+  doesKnowCommand(dogCommand: SIT)
+  doesKnowCommand(dogCommand: HEEL)
+}
+
+fragment conflictingArgsValueAndVar on Dog {
+  doesKnowCommand(dogCommand: SIT)
+  doesKnowCommand(dogCommand: $dogCommand)
+}
+
+fragment conflictingArgsWithVars on Dog {
+  doesKnowCommand(dogCommand: $varOne)
+  doesKnowCommand(dogCommand: $varTwo)
+}
+
+fragment differingArgs on Dog {
+  doesKnowCommand(dogCommand: SIT)
+  doesKnowCommand
+}
+
+fragment conflictingArgs on Dog {
+  isAtLocation(x: 0)
+  isAtLocation(y: 0)
+}
diff --git a/crates/apollo-compiler/test_data/diagnostics/0075_merge_conflicting_args.txt b/crates/apollo-compiler/test_data/diagnostics/0075_merge_conflicting_args.txt
new file mode 100644
index 0000000..f7076f1
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0075_merge_conflicting_args.txt
@@ -0,0 +1,272 @@
+[
+    ApolloDiagnostic {
+        cache: {
+            71: "0075_merge_conflicting_args.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 71,
+            },
+            offset: 218,
+            length: 33,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 199,
+                    length: 15,
+                },
+                text: "field `doesKnowCommand` provides one argument value here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 234,
+                    length: 16,
+                },
+                text: "but a different value here",
+            },
+        ],
+        help: Some(
+            "Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.",
+        ),
+        data: ConflictingField {
+            field: "doesKnowCommand",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 183,
+                length: 32,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 218,
+                length: 33,
+            },
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            71: "0075_merge_conflicting_args.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 71,
+            },
+            offset: 337,
+            length: 40,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 318,
+                    length: 15,
+                },
+                text: "field `doesKnowCommand` provides one argument value here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 353,
+                    length: 23,
+                },
+                text: "but a different value here",
+            },
+        ],
+        help: Some(
+            "Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.",
+        ),
+        data: ConflictingField {
+            field: "doesKnowCommand",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 302,
+                length: 32,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 337,
+                length: 40,
+            },
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            71: "0075_merge_conflicting_args.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 71,
+            },
+            offset: 464,
+            length: 36,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 441,
+                    length: 19,
+                },
+                text: "field `doesKnowCommand` provides one argument value here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 480,
+                    length: 19,
+                },
+                text: "but a different value here",
+            },
+        ],
+        help: Some(
+            "Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.",
+        ),
+        data: ConflictingField {
+            field: "doesKnowCommand",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 425,
+                length: 36,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 464,
+                length: 36,
+            },
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            71: "0075_merge_conflicting_args.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 71,
+            },
+            offset: 573,
+            length: 15,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 554,
+                    length: 15,
+                },
+                text: "field `doesKnowCommand` is selected with argument `dogCommand` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 573,
+                    length: 15,
+                },
+                text: "but argument `dogCommand` is not provided here",
+            },
+        ],
+        help: Some(
+            "Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.",
+        ),
+        data: ConflictingField {
+            field: "doesKnowCommand",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 538,
+                length: 32,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 573,
+                length: 15,
+            },
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            71: "0075_merge_conflicting_args.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 71,
+            },
+            offset: 649,
+            length: 18,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 641,
+                    length: 4,
+                },
+                text: "field `isAtLocation` is selected with argument `x` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 71,
+                    },
+                    offset: 649,
+                    length: 18,
+                },
+                text: "but argument `x` is not provided here",
+            },
+        ],
+        help: Some(
+            "Fields with the same response name must provide the same set of arguments. Consider adding an alias if you need to select fields with different arguments.",
+        ),
+        data: ConflictingField {
+            field: "isAtLocation",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 628,
+                length: 18,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 71,
+                },
+                offset: 649,
+                length: 18,
+            },
+        },
+    },
+]
diff --git a/crates/apollo-compiler/test_data/diagnostics/0076_merge_differing_responses.graphql b/crates/apollo-compiler/test_data/diagnostics/0076_merge_differing_responses.graphql
new file mode 100644
index 0000000..07ae144
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0076_merge_differing_responses.graphql
@@ -0,0 +1,34 @@
+interface Pet {
+  name: String!
+}
+
+enum DogCommand {
+  SIT
+  HEEL
+}
+
+enum CatCommand {
+  JUMP
+}
+
+type Dog implements Pet {
+  name: String!
+  nickname: String!
+  barkVolume: Int!
+  doesKnowCommand(dogCommand: DogCommand!): Boolean!
+}
+
+type Cat implements Pet {
+  name: String!
+  meowVolume: Int!
+  doesKnowCommand(catCommand: CatCommand!): Boolean!
+}
+
+fragment conflictingDifferingResponses on Pet {
+  ... on Dog {
+    someValue: nickname
+  }
+  ... on Cat {
+    someValue: meowVolume
+  }
+}
diff --git a/crates/apollo-compiler/test_data/diagnostics/0076_merge_differing_responses.txt b/crates/apollo-compiler/test_data/diagnostics/0076_merge_differing_responses.txt
new file mode 100644
index 0000000..c60b42f
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0076_merge_differing_responses.txt
@@ -0,0 +1,54 @@
+[
+    ApolloDiagnostic {
+        cache: {
+            72: "0076_merge_differing_responses.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 72,
+            },
+            offset: 461,
+            length: 21,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 72,
+                    },
+                    offset: 418,
+                    length: 19,
+                },
+                text: "`someValue` has type `String!` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 72,
+                    },
+                    offset: 461,
+                    length: 21,
+                },
+                text: "but the same field name has type `Int!` here",
+            },
+        ],
+        help: None,
+        data: ConflictingField {
+            field: "nickname",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 72,
+                },
+                offset: 418,
+                length: 19,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 72,
+                },
+                offset: 461,
+                length: 21,
+            },
+        },
+    },
+]
diff --git a/crates/apollo-compiler/test_data/diagnostics/0077_merge_conflict_deep.graphql b/crates/apollo-compiler/test_data/diagnostics/0077_merge_conflict_deep.graphql
new file mode 100644
index 0000000..fdf1d09
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0077_merge_conflict_deep.graphql
@@ -0,0 +1,16 @@
+type O {
+  a: Int
+  b: Int
+}
+type Query {
+  field: O,
+}
+
+{
+  field {
+    x: a
+  },
+  field {
+    x: b
+  }
+}
diff --git a/crates/apollo-compiler/test_data/diagnostics/0077_merge_conflict_deep.txt b/crates/apollo-compiler/test_data/diagnostics/0077_merge_conflict_deep.txt
new file mode 100644
index 0000000..a089040
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0077_merge_conflict_deep.txt
@@ -0,0 +1,56 @@
+[
+    ApolloDiagnostic {
+        cache: {
+            73: "0077_merge_conflict_deep.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 73,
+            },
+            offset: 97,
+            length: 4,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 73,
+                    },
+                    offset: 73,
+                    length: 4,
+                },
+                text: "field `x` is selected from field `a` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 73,
+                    },
+                    offset: 97,
+                    length: 4,
+                },
+                text: "but the same field `x` is also selected from field `b` here",
+            },
+        ],
+        help: Some(
+            "Alias is already used for a different field",
+        ),
+        data: ConflictingField {
+            field: "b",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 73,
+                },
+                offset: 73,
+                length: 4,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 73,
+                },
+                offset: 97,
+                length: 4,
+            },
+        },
+    },
+]
diff --git a/crates/apollo-compiler/test_data/diagnostics/0078_merge_conflict_nested_fragments.graphql b/crates/apollo-compiler/test_data/diagnostics/0078_merge_conflict_nested_fragments.graphql
new file mode 100644
index 0000000..10a2c09
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0078_merge_conflict_nested_fragments.graphql
@@ -0,0 +1,33 @@
+type T {
+  a: Int
+  b: Int
+  c: Int
+  d: Int
+}
+
+type Query {
+  field: T
+}
+
+{
+  field {
+    ...F
+  }
+  field {
+    ...I
+  }
+}
+fragment F on T {
+  x: a
+  ...G
+}
+fragment G on T {
+  y: c
+}
+fragment I on T {
+  y: d
+  ...J
+}
+fragment J on T {
+  x: b
+}
diff --git a/crates/apollo-compiler/test_data/diagnostics/0078_merge_conflict_nested_fragments.txt b/crates/apollo-compiler/test_data/diagnostics/0078_merge_conflict_nested_fragments.txt
new file mode 100644
index 0000000..dbf6022
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0078_merge_conflict_nested_fragments.txt
@@ -0,0 +1,110 @@
+[
+    ApolloDiagnostic {
+        cache: {
+            74: "0078_merge_conflict_nested_fragments.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 74,
+            },
+            offset: 240,
+            length: 4,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 74,
+                    },
+                    offset: 145,
+                    length: 4,
+                },
+                text: "field `x` is selected from field `a` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 74,
+                    },
+                    offset: 240,
+                    length: 4,
+                },
+                text: "but the same field `x` is also selected from field `b` here",
+            },
+        ],
+        help: Some(
+            "Alias is already used for a different field",
+        ),
+        data: ConflictingField {
+            field: "b",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 74,
+                },
+                offset: 145,
+                length: 4,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 74,
+                },
+                offset: 240,
+                length: 4,
+            },
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            74: "0078_merge_conflict_nested_fragments.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 74,
+            },
+            offset: 206,
+            length: 4,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 74,
+                    },
+                    offset: 179,
+                    length: 4,
+                },
+                text: "field `y` is selected from field `c` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 74,
+                    },
+                    offset: 206,
+                    length: 4,
+                },
+                text: "but the same field `y` is also selected from field `d` here",
+            },
+        ],
+        help: Some(
+            "Alias is already used for a different field",
+        ),
+        data: ConflictingField {
+            field: "d",
+            original_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 74,
+                },
+                offset: 179,
+                length: 4,
+            },
+            redefined_selection: DiagnosticLocation {
+                file_id: FileId {
+                    id: 74,
+                },
+                offset: 206,
+                length: 4,
+            },
+        },
+    },
+]
diff --git a/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.graphql b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.graphql
new file mode 100644
index 0000000..a533cc6
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.graphql
@@ -0,0 +1,14 @@
+type Dog {
+  nickname: String
+  name: String!
+}
+
+fragment mergeIdenticalFields on Dog {
+  name
+  name
+}
+
+fragment mergeIdenticalAliasesAndFields on Dog {
+  otherName: name
+  otherName: name
+}
diff --git a/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt
new file mode 100644
index 0000000..ad2caa4
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt
@@ -0,0 +1,97 @@
+- DOCUMENT@0..192
+    - OBJECT_TYPE_DEFINITION@0..47
+        - type_KW@0..4 "type"
+        - WHITESPACE@4..5 " "
+        - NAME@5..8
+            - IDENT@5..8 "Dog"
+        - WHITESPACE@8..9 " "
+        - FIELDS_DEFINITION@9..47
+            - L_CURLY@9..10 "{"
+            - WHITESPACE@10..13 "\n  "
+            - FIELD_DEFINITION@13..29
+                - NAME@13..21
+                    - IDENT@13..21 "nickname"
+                - COLON@21..22 ":"
+                - WHITESPACE@22..23 " "
+                - NAMED_TYPE@23..29
+                    - NAME@23..29
+                        - IDENT@23..29 "String"
+            - WHITESPACE@29..32 "\n  "
+            - FIELD_DEFINITION@32..45
+                - NAME@32..36
+                    - IDENT@32..36 "name"
+                - COLON@36..37 ":"
+                - WHITESPACE@37..38 " "
+                - NON_NULL_TYPE@38..45
+                    - NAMED_TYPE@38..44
+                        - NAME@38..44
+                            - IDENT@38..44 "String"
+                    - BANG@44..45 "!"
+            - WHITESPACE@45..46 "\n"
+            - R_CURLY@46..47 "}"
+    - WHITESPACE@47..49 "\n\n"
+    - FRAGMENT_DEFINITION@49..103
+        - fragment_KW@49..57 "fragment"
+        - WHITESPACE@57..58 " "
+        - FRAGMENT_NAME@58..78
+            - NAME@58..78
+                - IDENT@58..78 "mergeIdenticalFields"
+        - WHITESPACE@78..79 " "
+        - TYPE_CONDITION@79..85
+            - on_KW@79..81 "on"
+            - WHITESPACE@81..82 " "
+            - NAMED_TYPE@82..85
+                - NAME@82..85
+                    - IDENT@82..85 "Dog"
+        - WHITESPACE@85..86 " "
+        - SELECTION_SET@86..103
+            - L_CURLY@86..87 "{"
+            - WHITESPACE@87..90 "\n  "
+            - FIELD@90..94
+                - NAME@90..94
+                    - IDENT@90..94 "name"
+            - WHITESPACE@94..97 "\n  "
+            - FIELD@97..101
+                - NAME@97..101
+                    - IDENT@97..101 "name"
+            - WHITESPACE@101..102 "\n"
+            - R_CURLY@102..103 "}"
+    - WHITESPACE@103..105 "\n\n"
+    - FRAGMENT_DEFINITION@105..191
+        - fragment_KW@105..113 "fragment"
+        - WHITESPACE@113..114 " "
+        - FRAGMENT_NAME@114..144
+            - NAME@114..144
+                - IDENT@114..144 "mergeIdenticalAliasesAndFields"
+        - WHITESPACE@144..145 " "
+        - TYPE_CONDITION@145..151
+            - on_KW@145..147 "on"
+            - WHITESPACE@147..148 " "
+            - NAMED_TYPE@148..151
+                - NAME@148..151
+                    - IDENT@148..151 "Dog"
+        - WHITESPACE@151..152 " "
+        - SELECTION_SET@152..191
+            - L_CURLY@152..153 "{"
+            - WHITESPACE@153..156 "\n  "
+            - FIELD@156..171
+                - ALIAS@156..166
+                    - NAME@156..165
+                        - IDENT@156..165 "otherName"
+                    - COLON@165..166 ":"
+                - WHITESPACE@166..167 " "
+                - NAME@167..171
+                    - IDENT@167..171 "name"
+            - WHITESPACE@171..174 "\n  "
+            - FIELD@174..189
+                - ALIAS@174..184
+                    - NAME@174..183
+                        - IDENT@174..183 "otherName"
+                    - COLON@183..184 ":"
+                - WHITESPACE@184..185 " "
+                - NAME@185..189
+                    - IDENT@185..189 "name"
+            - WHITESPACE@189..190 "\n"
+            - R_CURLY@190..191 "}"
+    - WHITESPACE@191..192 "\n"
+recursion limit: 4096, high: 3
diff --git a/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.graphql b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.graphql
new file mode 100644
index 0000000..458f570
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.graphql
@@ -0,0 +1,18 @@
+enum DogCommand {
+  SIT
+  HEEL
+}
+
+type Dog {
+  doesKnowCommand(dogCommand: DogCommand): Boolean
+}
+
+fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
+  doesKnowCommand(dogCommand: SIT)
+  doesKnowCommand(dogCommand: SIT)
+}
+
+fragment mergeIdenticalFieldsWithIdenticalValues on Dog {
+  doesKnowCommand(dogCommand: $dogCommand)
+  doesKnowCommand(dogCommand: $dogCommand)
+}
diff --git a/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt
new file mode 100644
index 0000000..32de27e
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt
@@ -0,0 +1,154 @@
+- DOCUMENT@0..374
+    - ENUM_TYPE_DEFINITION@0..32
+        - enum_KW@0..4 "enum"
+        - WHITESPACE@4..5 " "
+        - NAME@5..15
+            - IDENT@5..15 "DogCommand"
+        - WHITESPACE@15..16 " "
+        - ENUM_VALUES_DEFINITION@16..32
+            - L_CURLY@16..17 "{"
+            - WHITESPACE@17..20 "\n  "
+            - ENUM_VALUE_DEFINITION@20..23
+                - ENUM_VALUE@20..23
+                    - NAME@20..23
+                        - IDENT@20..23 "SIT"
+            - WHITESPACE@23..26 "\n  "
+            - ENUM_VALUE_DEFINITION@26..30
+                - ENUM_VALUE@26..30
+                    - NAME@26..30
+                        - IDENT@26..30 "HEEL"
+            - WHITESPACE@30..31 "\n"
+            - R_CURLY@31..32 "}"
+    - WHITESPACE@32..34 "\n\n"
+    - OBJECT_TYPE_DEFINITION@34..97
+        - type_KW@34..38 "type"
+        - WHITESPACE@38..39 " "
+        - NAME@39..42
+            - IDENT@39..42 "Dog"
+        - WHITESPACE@42..43 " "
+        - FIELDS_DEFINITION@43..97
+            - L_CURLY@43..44 "{"
+            - WHITESPACE@44..47 "\n  "
+            - FIELD_DEFINITION@47..95
+                - NAME@47..62
+                    - IDENT@47..62 "doesKnowCommand"
+                - ARGUMENTS_DEFINITION@62..86
+                    - L_PAREN@62..63 "("
+                    - INPUT_VALUE_DEFINITION@63..85
+                        - NAME@63..73
+                            - IDENT@63..73 "dogCommand"
+                        - COLON@73..74 ":"
+                        - WHITESPACE@74..75 " "
+                        - NAMED_TYPE@75..85
+                            - NAME@75..85
+                                - IDENT@75..85 "DogCommand"
+                    - R_PAREN@85..86 ")"
+                - COLON@86..87 ":"
+                - WHITESPACE@87..88 " "
+                - NAMED_TYPE@88..95
+                    - NAME@88..95
+                        - IDENT@88..95 "Boolean"
+            - WHITESPACE@95..96 "\n"
+            - R_CURLY@96..97 "}"
+    - WHITESPACE@97..99 "\n\n"
+    - FRAGMENT_DEFINITION@99..226
+        - fragment_KW@99..107 "fragment"
+        - WHITESPACE@107..108 " "
+        - FRAGMENT_NAME@108..145
+            - NAME@108..145
+                - IDENT@108..145 "mergeIdenticalFieldsWithIdenticalArgs"
+        - WHITESPACE@145..146 " "
+        - TYPE_CONDITION@146..152
+            - on_KW@146..148 "on"
+            - WHITESPACE@148..149 " "
+            - NAMED_TYPE@149..152
+                - NAME@149..152
+                    - IDENT@149..152 "Dog"
+        - WHITESPACE@152..153 " "
+        - SELECTION_SET@153..226
+            - L_CURLY@153..154 "{"
+            - WHITESPACE@154..157 "\n  "
+            - FIELD@157..189
+                - NAME@157..172
+                    - IDENT@157..172 "doesKnowCommand"
+                - ARGUMENTS@172..189
+                    - L_PAREN@172..173 "("
+                    - ARGUMENT@173..188
+                        - NAME@173..183
+                            - IDENT@173..183 "dogCommand"
+                        - COLON@183..184 ":"
+                        - WHITESPACE@184..185 " "
+                        - ENUM_VALUE@185..188
+                            - NAME@185..188
+                                - IDENT@185..188 "SIT"
+                    - R_PAREN@188..189 ")"
+            - WHITESPACE@189..192 "\n  "
+            - FIELD@192..224
+                - NAME@192..207
+                    - IDENT@192..207 "doesKnowCommand"
+                - ARGUMENTS@207..224
+                    - L_PAREN@207..208 "("
+                    - ARGUMENT@208..223
+                        - NAME@208..218
+                            - IDENT@208..218 "dogCommand"
+                        - COLON@218..219 ":"
+                        - WHITESPACE@219..220 " "
+                        - ENUM_VALUE@220..223
+                            - NAME@220..223
+                                - IDENT@220..223 "SIT"
+                    - R_PAREN@223..224 ")"
+            - WHITESPACE@224..225 "\n"
+            - R_CURLY@225..226 "}"
+    - WHITESPACE@226..228 "\n\n"
+    - FRAGMENT_DEFINITION@228..373
+        - fragment_KW@228..236 "fragment"
+        - WHITESPACE@236..237 " "
+        - FRAGMENT_NAME@237..276
+            - NAME@237..276
+                - IDENT@237..276 "mergeIdenticalFieldsWithIdenticalValues"
+        - WHITESPACE@276..277 " "
+        - TYPE_CONDITION@277..283
+            - on_KW@277..279 "on"
+            - WHITESPACE@279..280 " "
+            - NAMED_TYPE@280..283
+                - NAME@280..283
+                    - IDENT@280..283 "Dog"
+        - WHITESPACE@283..284 " "
+        - SELECTION_SET@284..373
+            - L_CURLY@284..285 "{"
+            - WHITESPACE@285..288 "\n  "
+            - FIELD@288..328
+                - NAME@288..303
+                    - IDENT@288..303 "doesKnowCommand"
+                - ARGUMENTS@303..328
+                    - L_PAREN@303..304 "("
+                    - ARGUMENT@304..327
+                        - NAME@304..314
+                            - IDENT@304..314 "dogCommand"
+                        - COLON@314..315 ":"
+                        - WHITESPACE@315..316 " "
+                        - VARIABLE@316..327
+                            - DOLLAR@316..317 "$"
+                            - NAME@317..327
+                                - IDENT@317..327 "dogCommand"
+                    - R_PAREN@327..328 ")"
+            - WHITESPACE@328..331 "\n  "
+            - FIELD@331..371
+                - NAME@331..346
+                    - IDENT@331..346 "doesKnowCommand"
+                - ARGUMENTS@346..371
+                    - L_PAREN@346..347 "("
+                    - ARGUMENT@347..370
+                        - NAME@347..357
+                            - IDENT@347..357 "dogCommand"
+                        - COLON@357..358 ":"
+                        - WHITESPACE@358..359 " "
+                        - VARIABLE@359..370
+                            - DOLLAR@359..360 "$"
+                            - NAME@360..370
+                                - IDENT@360..370 "dogCommand"
+                    - R_PAREN@370..371 ")"
+            - WHITESPACE@371..372 "\n"
+            - R_CURLY@372..373 "}"
+    - WHITESPACE@373..374 "\n"
+recursion limit: 4096, high: 3
diff --git a/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.graphql b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.graphql
new file mode 100644
index 0000000..21821d0
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.graphql
@@ -0,0 +1,49 @@
+interface Pet {
+  name: String!
+}
+
+enum DogCommand {
+  SIT
+  HEEL
+}
+
+enum CatCommand {
+  JUMP
+}
+
+type Dog implements Pet {
+  name: String!
+  nickname: String!
+  barkVolume: Int!
+  doesKnowCommand(dogCommand: DogCommand!): Boolean!
+  isAtLocation(x: Int, y: Int): Boolean!
+}
+
+type Cat implements Pet {
+  name: String!
+  meowVolume: Int!
+  doesKnowCommand(catCommand: CatCommand!): Boolean!
+}
+
+fragment safeDifferingFields on Pet {
+  ... on Dog {
+    volume: barkVolume
+  }
+  ... on Cat {
+    volume: meowVolume
+  }
+}
+
+fragment safeDifferingArgs on Pet {
+  ... on Dog {
+    doesKnowCommand(dogCommand: SIT)
+  }
+  ... on Cat {
+    doesKnowCommand(catCommand: JUMP)
+  }
+}
+
+fragment safeDifferingArgOrder on Dog {
+  isAtLocation(x: 0, y: 0)
+  isAtLocation(y: 0, x: 0)
+}
diff --git a/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt
new file mode 100644
index 0000000..ec4484c
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt
@@ -0,0 +1,443 @@
+- DOCUMENT@0..765
+    - INTERFACE_TYPE_DEFINITION@0..33
+        - interface_KW@0..9 "interface"
+        - WHITESPACE@9..10 " "
+        - NAME@10..13
+            - IDENT@10..13 "Pet"
+        - WHITESPACE@13..14 " "
+        - FIELDS_DEFINITION@14..33
+            - L_CURLY@14..15 "{"
+            - WHITESPACE@15..18 "\n  "
+            - FIELD_DEFINITION@18..31
+                - NAME@18..22
+                    - IDENT@18..22 "name"
+                - COLON@22..23 ":"
+                - WHITESPACE@23..24 " "
+                - NON_NULL_TYPE@24..31
+                    - NAMED_TYPE@24..30
+                        - NAME@24..30
+                            - IDENT@24..30 "String"
+                    - BANG@30..31 "!"
+            - WHITESPACE@31..32 "\n"
+            - R_CURLY@32..33 "}"
+    - WHITESPACE@33..35 "\n\n"
+    - ENUM_TYPE_DEFINITION@35..67
+        - enum_KW@35..39 "enum"
+        - WHITESPACE@39..40 " "
+        - NAME@40..50
+            - IDENT@40..50 "DogCommand"
+        - WHITESPACE@50..51 " "
+        - ENUM_VALUES_DEFINITION@51..67
+            - L_CURLY@51..52 "{"
+            - WHITESPACE@52..55 "\n  "
+            - ENUM_VALUE_DEFINITION@55..58
+                - ENUM_VALUE@55..58
+                    - NAME@55..58
+                        - IDENT@55..58 "SIT"
+            - WHITESPACE@58..61 "\n  "
+            - ENUM_VALUE_DEFINITION@61..65
+                - ENUM_VALUE@61..65
+                    - NAME@61..65
+                        - IDENT@61..65 "HEEL"
+            - WHITESPACE@65..66 "\n"
+            - R_CURLY@66..67 "}"
+    - WHITESPACE@67..69 "\n\n"
+    - ENUM_TYPE_DEFINITION@69..95
+        - enum_KW@69..73 "enum"
+        - WHITESPACE@73..74 " "
+        - NAME@74..84
+            - IDENT@74..84 "CatCommand"
+        - WHITESPACE@84..85 " "
+        - ENUM_VALUES_DEFINITION@85..95
+            - L_CURLY@85..86 "{"
+            - WHITESPACE@86..89 "\n  "
+            - ENUM_VALUE_DEFINITION@89..93
+                - ENUM_VALUE@89..93
+                    - NAME@89..93
+                        - IDENT@89..93 "JUMP"
+            - WHITESPACE@93..94 "\n"
+            - R_CURLY@94..95 "}"
+    - WHITESPACE@95..97 "\n\n"
+    - OBJECT_TYPE_DEFINITION@97..273
+        - type_KW@97..101 "type"
+        - WHITESPACE@101..102 " "
+        - NAME@102..105
+            - IDENT@102..105 "Dog"
+        - WHITESPACE@105..106 " "
+        - IMPLEMENTS_INTERFACES@106..120
+            - implements_KW@106..116 "implements"
+            - WHITESPACE@116..117 " "
+            - NAMED_TYPE@117..120
+                - NAME@117..120
+                    - IDENT@117..120 "Pet"
+        - WHITESPACE@120..121 " "
+        - FIELDS_DEFINITION@121..273
+            - L_CURLY@121..122 "{"
+            - WHITESPACE@122..125 "\n  "
+            - FIELD_DEFINITION@125..138
+                - NAME@125..129
+                    - IDENT@125..129 "name"
+                - COLON@129..130 ":"
+                - WHITESPACE@130..131 " "
+                - NON_NULL_TYPE@131..138
+                    - NAMED_TYPE@131..137
+                        - NAME@131..137
+                            - IDENT@131..137 "String"
+                    - BANG@137..138 "!"
+            - WHITESPACE@138..141 "\n  "
+            - FIELD_DEFINITION@141..158
+                - NAME@141..149
+                    - IDENT@141..149 "nickname"
+                - COLON@149..150 ":"
+                - WHITESPACE@150..151 " "
+                - NON_NULL_TYPE@151..158
+                    - NAMED_TYPE@151..157
+                        - NAME@151..157
+                            - IDENT@151..157 "String"
+                    - BANG@157..158 "!"
+            - WHITESPACE@158..161 "\n  "
+            - FIELD_DEFINITION@161..177
+                - NAME@161..171
+                    - IDENT@161..171 "barkVolume"
+                - COLON@171..172 ":"
+                - WHITESPACE@172..173 " "
+                - NON_NULL_TYPE@173..177
+                    - NAMED_TYPE@173..176
+                        - NAME@173..176
+                            - IDENT@173..176 "Int"
+                    - BANG@176..177 "!"
+            - WHITESPACE@177..180 "\n  "
+            - FIELD_DEFINITION@180..230
+                - NAME@180..195
+                    - IDENT@180..195 "doesKnowCommand"
+                - ARGUMENTS_DEFINITION@195..220
+                    - L_PAREN@195..196 "("
+                    - INPUT_VALUE_DEFINITION@196..219
+                        - NAME@196..206
+                            - IDENT@196..206 "dogCommand"
+                        - COLON@206..207 ":"
+                        - WHITESPACE@207..208 " "
+                        - NON_NULL_TYPE@208..219
+                            - NAMED_TYPE@208..218
+                                - NAME@208..218
+                                    - IDENT@208..218 "DogCommand"
+                            - BANG@218..219 "!"
+                    - R_PAREN@219..220 ")"
+                - COLON@220..221 ":"
+                - WHITESPACE@221..222 " "
+                - NON_NULL_TYPE@222..230
+                    - NAMED_TYPE@222..229
+                        - NAME@222..229
+                            - IDENT@222..229 "Boolean"
+                    - BANG@229..230 "!"
+            - WHITESPACE@230..233 "\n  "
+            - FIELD_DEFINITION@233..271
+                - NAME@233..245
+                    - IDENT@233..245 "isAtLocation"
+                - ARGUMENTS_DEFINITION@245..261
+                    - L_PAREN@245..246 "("
+                    - INPUT_VALUE_DEFINITION@246..252
+                        - NAME@246..247
+                            - IDENT@246..247 "x"
+                        - COLON@247..248 ":"
+                        - WHITESPACE@248..249 " "
+                        - NAMED_TYPE@249..252
+                            - NAME@249..252
+                                - IDENT@249..252 "Int"
+                    - COMMA@252..253 ","
+                    - WHITESPACE@253..254 " "
+                    - INPUT_VALUE_DEFINITION@254..260
+                        - NAME@254..255
+                            - IDENT@254..255 "y"
+                        - COLON@255..256 ":"
+                        - WHITESPACE@256..257 " "
+                        - NAMED_TYPE@257..260
+                            - NAME@257..260
+                                - IDENT@257..260 "Int"
+                    - R_PAREN@260..261 ")"
+                - COLON@261..262 ":"
+                - WHITESPACE@262..263 " "
+                - NON_NULL_TYPE@263..271
+                    - NAMED_TYPE@263..270
+                        - NAME@263..270
+                            - IDENT@263..270 "Boolean"
+                    - BANG@270..271 "!"
+            - WHITESPACE@271..272 "\n"
+            - R_CURLY@272..273 "}"
+    - WHITESPACE@273..275 "\n\n"
+    - OBJECT_TYPE_DEFINITION@275..390
+        - type_KW@275..279 "type"
+        - WHITESPACE@279..280 " "
+        - NAME@280..283
+            - IDENT@280..283 "Cat"
+        - WHITESPACE@283..284 " "
+        - IMPLEMENTS_INTERFACES@284..298
+            - implements_KW@284..294 "implements"
+            - WHITESPACE@294..295 " "
+            - NAMED_TYPE@295..298
+                - NAME@295..298
+                    - IDENT@295..298 "Pet"
+        - WHITESPACE@298..299 " "
+        - FIELDS_DEFINITION@299..390
+            - L_CURLY@299..300 "{"
+            - WHITESPACE@300..303 "\n  "
+            - FIELD_DEFINITION@303..316
+                - NAME@303..307
+                    - IDENT@303..307 "name"
+                - COLON@307..308 ":"
+                - WHITESPACE@308..309 " "
+                - NON_NULL_TYPE@309..316
+                    - NAMED_TYPE@309..315
+                        - NAME@309..315
+                            - IDENT@309..315 "String"
+                    - BANG@315..316 "!"
+            - WHITESPACE@316..319 "\n  "
+            - FIELD_DEFINITION@319..335
+                - NAME@319..329
+                    - IDENT@319..329 "meowVolume"
+                - COLON@329..330 ":"
+                - WHITESPACE@330..331 " "
+                - NON_NULL_TYPE@331..335
+                    - NAMED_TYPE@331..334
+                        - NAME@331..334
+                            - IDENT@331..334 "Int"
+                    - BANG@334..335 "!"
+            - WHITESPACE@335..338 "\n  "
+            - FIELD_DEFINITION@338..388
+                - NAME@338..353
+                    - IDENT@338..353 "doesKnowCommand"
+                - ARGUMENTS_DEFINITION@353..378
+                    - L_PAREN@353..354 "("
+                    - INPUT_VALUE_DEFINITION@354..377
+                        - NAME@354..364
+                            - IDENT@354..364 "catCommand"
+                        - COLON@364..365 ":"
+                        - WHITESPACE@365..366 " "
+                        - NON_NULL_TYPE@366..377
+                            - NAMED_TYPE@366..376
+                                - NAME@366..376
+                                    - IDENT@366..376 "CatCommand"
+                            - BANG@376..377 "!"
+                    - R_PAREN@377..378 ")"
+                - COLON@378..379 ":"
+                - WHITESPACE@379..380 " "
+                - NON_NULL_TYPE@380..388
+                    - NAMED_TYPE@380..387
+                        - NAME@380..387
+                            - IDENT@380..387 "Boolean"
+                    - BANG@387..388 "!"
+            - WHITESPACE@388..389 "\n"
+            - R_CURLY@389..390 "}"
+    - WHITESPACE@390..392 "\n\n"
+    - FRAGMENT_DEFINITION@392..515
+        - fragment_KW@392..400 "fragment"
+        - WHITESPACE@400..401 " "
+        - FRAGMENT_NAME@401..420
+            - NAME@401..420
+                - IDENT@401..420 "safeDifferingFields"
+        - WHITESPACE@420..421 " "
+        - TYPE_CONDITION@421..427
+            - on_KW@421..423 "on"
+            - WHITESPACE@423..424 " "
+            - NAMED_TYPE@424..427
+                - NAME@424..427
+                    - IDENT@424..427 "Pet"
+        - WHITESPACE@427..428 " "
+        - SELECTION_SET@428..515
+            - L_CURLY@428..429 "{"
+            - WHITESPACE@429..432 "\n  "
+            - INLINE_FRAGMENT@432..471
+                - SPREAD@432..435 "..."
+                - WHITESPACE@435..436 " "
+                - TYPE_CONDITION@436..442
+                    - on_KW@436..438 "on"
+                    - WHITESPACE@438..439 " "
+                    - NAMED_TYPE@439..442
+                        - NAME@439..442
+                            - IDENT@439..442 "Dog"
+                - WHITESPACE@442..443 " "
+                - SELECTION_SET@443..471
+                    - L_CURLY@443..444 "{"
+                    - WHITESPACE@444..449 "\n    "
+                    - FIELD@449..467
+                        - ALIAS@449..456
+                            - NAME@449..455
+                                - IDENT@449..455 "volume"
+                            - COLON@455..456 ":"
+                        - WHITESPACE@456..457 " "
+                        - NAME@457..467
+                            - IDENT@457..467 "barkVolume"
+                    - WHITESPACE@467..470 "\n  "
+                    - R_CURLY@470..471 "}"
+            - WHITESPACE@471..474 "\n  "
+            - INLINE_FRAGMENT@474..513
+                - SPREAD@474..477 "..."
+                - WHITESPACE@477..478 " "
+                - TYPE_CONDITION@478..484
+                    - on_KW@478..480 "on"
+                    - WHITESPACE@480..481 " "
+                    - NAMED_TYPE@481..484
+                        - NAME@481..484
+                            - IDENT@481..484 "Cat"
+                - WHITESPACE@484..485 " "
+                - SELECTION_SET@485..513
+                    - L_CURLY@485..486 "{"
+                    - WHITESPACE@486..491 "\n    "
+                    - FIELD@491..509
+                        - ALIAS@491..498
+                            - NAME@491..497
+                                - IDENT@491..497 "volume"
+                            - COLON@497..498 ":"
+                        - WHITESPACE@498..499 " "
+                        - NAME@499..509
+                            - IDENT@499..509 "meowVolume"
+                    - WHITESPACE@509..512 "\n  "
+                    - R_CURLY@512..513 "}"
+            - WHITESPACE@513..514 "\n"
+            - R_CURLY@514..515 "}"
+    - WHITESPACE@515..517 "\n\n"
+    - FRAGMENT_DEFINITION@517..667
+        - fragment_KW@517..525 "fragment"
+        - WHITESPACE@525..526 " "
+        - FRAGMENT_NAME@526..543
+            - NAME@526..543
+                - IDENT@526..543 "safeDifferingArgs"
+        - WHITESPACE@543..544 " "
+        - TYPE_CONDITION@544..550
+            - on_KW@544..546 "on"
+            - WHITESPACE@546..547 " "
+            - NAMED_TYPE@547..550
+                - NAME@547..550
+                    - IDENT@547..550 "Pet"
+        - WHITESPACE@550..551 " "
+        - SELECTION_SET@551..667
+            - L_CURLY@551..552 "{"
+            - WHITESPACE@552..555 "\n  "
+            - INLINE_FRAGMENT@555..608
+                - SPREAD@555..558 "..."
+                - WHITESPACE@558..559 " "
+                - TYPE_CONDITION@559..565
+                    - on_KW@559..561 "on"
+                    - WHITESPACE@561..562 " "
+                    - NAMED_TYPE@562..565
+                        - NAME@562..565
+                            - IDENT@562..565 "Dog"
+                - WHITESPACE@565..566 " "
+                - SELECTION_SET@566..608
+                    - L_CURLY@566..567 "{"
+                    - WHITESPACE@567..572 "\n    "
+                    - FIELD@572..604
+                        - NAME@572..587
+                            - IDENT@572..587 "doesKnowCommand"
+                        - ARGUMENTS@587..604
+                            - L_PAREN@587..588 "("
+                            - ARGUMENT@588..603
+                                - NAME@588..598
+                                    - IDENT@588..598 "dogCommand"
+                                - COLON@598..599 ":"
+                                - WHITESPACE@599..600 " "
+                                - ENUM_VALUE@600..603
+                                    - NAME@600..603
+                                        - IDENT@600..603 "SIT"
+                            - R_PAREN@603..604 ")"
+                    - WHITESPACE@604..607 "\n  "
+                    - R_CURLY@607..608 "}"
+            - WHITESPACE@608..611 "\n  "
+            - INLINE_FRAGMENT@611..665
+                - SPREAD@611..614 "..."
+                - WHITESPACE@614..615 " "
+                - TYPE_CONDITION@615..621
+                    - on_KW@615..617 "on"
+                    - WHITESPACE@617..618 " "
+                    - NAMED_TYPE@618..621
+                        - NAME@618..621
+                            - IDENT@618..621 "Cat"
+                - WHITESPACE@621..622 " "
+                - SELECTION_SET@622..665
+                    - L_CURLY@622..623 "{"
+                    - WHITESPACE@623..628 "\n    "
+                    - FIELD@628..661
+                        - NAME@628..643
+                            - IDENT@628..643 "doesKnowCommand"
+                        - ARGUMENTS@643..661
+                            - L_PAREN@643..644 "("
+                            - ARGUMENT@644..660
+                                - NAME@644..654
+                                    - IDENT@644..654 "catCommand"
+                                - COLON@654..655 ":"
+                                - WHITESPACE@655..656 " "
+                                - ENUM_VALUE@656..660
+                                    - NAME@656..660
+                                        - IDENT@656..660 "JUMP"
+                            - R_PAREN@660..661 ")"
+                    - WHITESPACE@661..664 "\n  "
+                    - R_CURLY@664..665 "}"
+            - WHITESPACE@665..666 "\n"
+            - R_CURLY@666..667 "}"
+    - WHITESPACE@667..669 "\n\n"
+    - FRAGMENT_DEFINITION@669..764
+        - fragment_KW@669..677 "fragment"
+        - WHITESPACE@677..678 " "
+        - FRAGMENT_NAME@678..699
+            - NAME@678..699
+                - IDENT@678..699 "safeDifferingArgOrder"
+        - WHITESPACE@699..700 " "
+        - TYPE_CONDITION@700..706
+            - on_KW@700..702 "on"
+            - WHITESPACE@702..703 " "
+            - NAMED_TYPE@703..706
+                - NAME@703..706
+                    - IDENT@703..706 "Dog"
+        - WHITESPACE@706..707 " "
+        - SELECTION_SET@707..764
+            - L_CURLY@707..708 "{"
+            - WHITESPACE@708..711 "\n  "
+            - FIELD@711..735
+                - NAME@711..723
+                    - IDENT@711..723 "isAtLocation"
+                - ARGUMENTS@723..735
+                    - L_PAREN@723..724 "("
+                    - ARGUMENT@724..728
+                        - NAME@724..725
+                            - IDENT@724..725 "x"
+                        - COLON@725..726 ":"
+                        - WHITESPACE@726..727 " "
+                        - INT_VALUE@727..728
+                            - INT@727..728 "0"
+                    - COMMA@728..729 ","
+                    - WHITESPACE@729..730 " "
+                    - ARGUMENT@730..734
+                        - NAME@730..731
+                            - IDENT@730..731 "y"
+                        - COLON@731..732 ":"
+                        - WHITESPACE@732..733 " "
+                        - INT_VALUE@733..734
+                            - INT@733..734 "0"
+                    - R_PAREN@734..735 ")"
+            - WHITESPACE@735..738 "\n  "
+            - FIELD@738..762
+                - NAME@738..750
+                    - IDENT@738..750 "isAtLocation"
+                - ARGUMENTS@750..762
+                    - L_PAREN@750..751 "("
+                    - ARGUMENT@751..755
+                        - NAME@751..752
+                            - IDENT@751..752 "y"
+                        - COLON@752..753 ":"
+                        - WHITESPACE@753..754 " "
+                        - INT_VALUE@754..755
+                            - INT@754..755 "0"
+                    - COMMA@755..756 ","
+                    - WHITESPACE@756..757 " "
+                    - ARGUMENT@757..761
+                        - NAME@757..758
+                            - IDENT@757..758 "x"
+                        - COLON@758..759 ":"
+                        - WHITESPACE@759..760 " "
+                        - INT_VALUE@760..761
+                            - INT@760..761 "0"
+                    - R_PAREN@761..762 ")"
+            - WHITESPACE@762..763 "\n"
+            - R_CURLY@763..764 "}"
+    - WHITESPACE@764..765 "\n"
+recursion limit: 4096, high: 3
diff --git a/crates/apollo-compiler/test_data/ok/0023_recursive_fragment.graphql b/crates/apollo-compiler/test_data/ok/0023_recursive_fragment.graphql
new file mode 100644
index 0000000..57bb3a9
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0023_recursive_fragment.graphql
@@ -0,0 +1,15 @@
+# Schema
+type Human {
+  name: String!
+}
+type Query {
+  human: Human
+}
+
+# Query
+{
+  human { ...fragA }
+}
+fragment fragA on Human { name, ...fragB }
+fragment fragB on Human { name, ...fragC }
+fragment fragC on Human { name, ...fragA }
diff --git a/crates/apollo-compiler/test_data/ok/0023_recursive_fragment.txt b/crates/apollo-compiler/test_data/ok/0023_recursive_fragment.txt
new file mode 100644
index 0000000..172f525
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0023_recursive_fragment.txt
@@ -0,0 +1,159 @@
+- DOCUMENT@0..233
+    - COMMENT@0..8 "# Schema"
+    - WHITESPACE@8..9 "\n"
+    - OBJECT_TYPE_DEFINITION@9..39
+        - type_KW@9..13 "type"
+        - WHITESPACE@13..14 " "
+        - NAME@14..19
+            - IDENT@14..19 "Human"
+        - WHITESPACE@19..20 " "
+        - FIELDS_DEFINITION@20..39
+            - L_CURLY@20..21 "{"
+            - WHITESPACE@21..24 "\n  "
+            - FIELD_DEFINITION@24..37
+                - NAME@24..28
+                    - IDENT@24..28 "name"
+                - COLON@28..29 ":"
+                - WHITESPACE@29..30 " "
+                - NON_NULL_TYPE@30..37
+                    - NAMED_TYPE@30..36
+                        - NAME@30..36
+                            - IDENT@30..36 "String"
+                    - BANG@36..37 "!"
+            - WHITESPACE@37..38 "\n"
+            - R_CURLY@38..39 "}"
+    - WHITESPACE@39..40 "\n"
+    - OBJECT_TYPE_DEFINITION@40..69
+        - type_KW@40..44 "type"
+        - WHITESPACE@44..45 " "
+        - NAME@45..50
+            - IDENT@45..50 "Query"
+        - WHITESPACE@50..51 " "
+        - FIELDS_DEFINITION@51..69
+            - L_CURLY@51..52 "{"
+            - WHITESPACE@52..55 "\n  "
+            - FIELD_DEFINITION@55..67
+                - NAME@55..60
+                    - IDENT@55..60 "human"
+                - COLON@60..61 ":"
+                - WHITESPACE@61..62 " "
+                - NAMED_TYPE@62..67
+                    - NAME@62..67
+                        - IDENT@62..67 "Human"
+            - WHITESPACE@67..68 "\n"
+            - R_CURLY@68..69 "}"
+    - WHITESPACE@69..71 "\n\n"
+    - COMMENT@71..78 "# Query"
+    - WHITESPACE@78..79 "\n"
+    - OPERATION_DEFINITION@79..103
+        - SELECTION_SET@79..103
+            - L_CURLY@79..80 "{"
+            - WHITESPACE@80..83 "\n  "
+            - FIELD@83..101
+                - NAME@83..88
+                    - IDENT@83..88 "human"
+                - WHITESPACE@88..89 " "
+                - SELECTION_SET@89..101
+                    - L_CURLY@89..90 "{"
+                    - WHITESPACE@90..91 " "
+                    - FRAGMENT_SPREAD@91..99
+                        - SPREAD@91..94 "..."
+                        - FRAGMENT_NAME@94..99
+                            - NAME@94..99
+                                - IDENT@94..99 "fragA"
+                    - WHITESPACE@99..100 " "
+                    - R_CURLY@100..101 "}"
+            - WHITESPACE@101..102 "\n"
+            - R_CURLY@102..103 "}"
+    - WHITESPACE@103..104 "\n"
+    - FRAGMENT_DEFINITION@104..146
+        - fragment_KW@104..112 "fragment"
+        - WHITESPACE@112..113 " "
+        - FRAGMENT_NAME@113..118
+            - NAME@113..118
+                - IDENT@113..118 "fragA"
+        - WHITESPACE@118..119 " "
+        - TYPE_CONDITION@119..127
+            - on_KW@119..121 "on"
+            - WHITESPACE@121..122 " "
+            - NAMED_TYPE@122..127
+                - NAME@122..127
+                    - IDENT@122..127 "Human"
+        - WHITESPACE@127..128 " "
+        - SELECTION_SET@128..146
+            - L_CURLY@128..129 "{"
+            - WHITESPACE@129..130 " "
+            - FIELD@130..134
+                - NAME@130..134
+                    - IDENT@130..134 "name"
+            - COMMA@134..135 ","
+            - WHITESPACE@135..136 " "
+            - FRAGMENT_SPREAD@136..144
+                - SPREAD@136..139 "..."
+                - FRAGMENT_NAME@139..144
+                    - NAME@139..144
+                        - IDENT@139..144 "fragB"
+            - WHITESPACE@144..145 " "
+            - R_CURLY@145..146 "}"
+    - WHITESPACE@146..147 "\n"
+    - FRAGMENT_DEFINITION@147..189
+        - fragment_KW@147..155 "fragment"
+        - WHITESPACE@155..156 " "
+        - FRAGMENT_NAME@156..161
+            - NAME@156..161
+                - IDENT@156..161 "fragB"
+        - WHITESPACE@161..162 " "
+        - TYPE_CONDITION@162..170
+            - on_KW@162..164 "on"
+            - WHITESPACE@164..165 " "
+            - NAMED_TYPE@165..170
+                - NAME@165..170
+                    - IDENT@165..170 "Human"
+        - WHITESPACE@170..171 " "
+        - SELECTION_SET@171..189
+            - L_CURLY@171..172 "{"
+            - WHITESPACE@172..173 " "
+            - FIELD@173..177
+                - NAME@173..177
+                    - IDENT@173..177 "name"
+            - COMMA@177..178 ","
+            - WHITESPACE@178..179 " "
+            - FRAGMENT_SPREAD@179..187
+                - SPREAD@179..182 "..."
+                - FRAGMENT_NAME@182..187
+                    - NAME@182..187
+                        - IDENT@182..187 "fragC"
+            - WHITESPACE@187..188 " "
+            - R_CURLY@188..189 "}"
+    - WHITESPACE@189..190 "\n"
+    - FRAGMENT_DEFINITION@190..232
+        - fragment_KW@190..198 "fragment"
+        - WHITESPACE@198..199 " "
+        - FRAGMENT_NAME@199..204
+            - NAME@199..204
+                - IDENT@199..204 "fragC"
+        - WHITESPACE@204..205 " "
+        - TYPE_CONDITION@205..213
+            - on_KW@205..207 "on"
+            - WHITESPACE@207..208 " "
+            - NAMED_TYPE@208..213
+                - NAME@208..213
+                    - IDENT@208..213 "Human"
+        - WHITESPACE@213..214 " "
+        - SELECTION_SET@214..232
+            - L_CURLY@214..215 "{"
+            - WHITESPACE@215..216 " "
+            - FIELD@216..220
+                - NAME@216..220
+                    - IDENT@216..220 "name"
+            - COMMA@220..221 ","
+            - WHITESPACE@221..222 " "
+            - FRAGMENT_SPREAD@222..230
+                - SPREAD@222..225 "..."
+                - FRAGMENT_NAME@225..230
+                    - NAME@225..230
+                        - IDENT@225..230 "fragA"
+            - WHITESPACE@230..231 " "
+            - R_CURLY@231..232 "}"
+    - WHITESPACE@232..233 "\n"
+recursion limit: 4096, high: 3