Repository: apollographql/apollo-rs
Pull request #748 (2023-11-17 15:22:33)

fix(compiler): apply limits to recursive functions in validation
Fixes https://github.com/apollographql/apollo-rs/issues/742

The validation code already had protections for cycles. However if you provided a very long chain of types or fragments referencing each other, it could chase each reference and still cause a stack overflow. This does pretty much require malicious input, but the failure mode is quite bad, so it's worth protecting against.

Self-referential directives and input objects now also show the chain that was followed in diagnostics. The directives one can be a bit incomplete because there may be a different kind of node in between (eg there might be a directive applied to an enum used inside an input type used as an argument to the same directive... and only directive names are tracked in the chain.) I think we can improve that with other diagnostics work soon, this is already an improvement.

goto-bus-stop: Also added tests for input objects and directives. Directives in particular didn't cause a stack overflow even with thousands of definitions, but they did take over 60 seconds to validate, so the limit also helps with that.

I lowered the limit to 200 because fragments could still overflow with 500 (the cycles detection works with 500, but the recursion in `validate_fragment_definition` is more expensive as it cycles through a few different and bigger methods).
Thinking about the serde_json limit, 200 is still a lot!

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index d0be3b1..b1f3258 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -108,12 +108,41 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
   [pull/752]:**
   when mutating isn’t needed after parsing,
   this returns an immutable `Valid<_>` value in one step.
+- **Serialize multi-line strings as block strings- [SimonSapin], [pull/724]**
+  Example before:
+  ```graphql
+  "Example\n\nDescription description description"
+  schema { query: MyQuery }
+  ```
+  After:
+  ```graphql
+  """
+  Example
+  
+  Description description description
+  """
+  schema { query: MyQuery }
+  ```
 
 [SimonSapin]: https://github.com/SimonSapin
 [issue/709]: https://github.com/apollographql/apollo-rs/issues/709
 [issue/751]: https://github.com/apollographql/apollo-rs/issues/751
+[pull/724]: https://github.com/apollographql/apollo-rs/pull/724
 [pull/752]: https://github.com/apollographql/apollo-rs/pull/752
 
+## Fixes
+
+- **Limit recursion in validation - [goto-bus-stop], [pull/748] fixing [issue/742]**
+  Validation now bails out of very long chains of definitions that refer to each other,
+  even if they don't strictly form a cycle. These could previously cause extremely long validation
+  times or stack overflows.
+
+  The limit for input objects and directives is set at 32. For fragments, the limit is set at 100.
+  Based on our datasets, real-world documents don't come anywhere close to this.
+
+[goto-bus-stop]: https://github.com/goto-bus-stop
+[issue/742]: https://github.com/apollographql/apollo-rs/issues/742
+[pull/748]: https://github.com/apollographql/apollo-rs/pull/748
 
 # [1.0.0-beta.7](https://crates.io/crates/apollo-compiler/1.0.0-beta.7) - 2023-11-17
 
diff --git a/crates/apollo-compiler/src/ast/serialize.rs b/crates/apollo-compiler/src/ast/serialize.rs
index 6900713..757fd0d 100644
--- a/crates/apollo-compiler/src/ast/serialize.rs
+++ b/crates/apollo-compiler/src/ast/serialize.rs
@@ -12,7 +12,7 @@ pub struct Serialize<'a, T> {
 
 #[derive(Debug, Clone)]
 pub(crate) struct Config<'a> {
-    ident_prefix: Option<&'a str>,
+    indent_prefix: Option<&'a str>,
 }
 
 pub(crate) struct State<'config, 'fmt, 'fmt2> {
@@ -29,13 +29,13 @@ impl<'a, T> Serialize<'a, T> {
     /// `prefix` is repeated at the start of each line by the number of indentation levels.
     /// The default is `"  "`, two spaces.
     pub fn indent_prefix(mut self, prefix: &'a str) -> Self {
-        self.config.ident_prefix = Some(prefix);
+        self.config.indent_prefix = Some(prefix);
         self
     }
 
     /// Disable indentation and line breaks
     pub fn no_indent(mut self) -> Self {
-        self.config.ident_prefix = None;
+        self.config.indent_prefix = None;
         self
     }
 }
@@ -43,7 +43,7 @@ impl<'a, T> Serialize<'a, T> {
 impl Default for Config<'_> {
     fn default() -> Self {
         Self {
-            ident_prefix: Some("  "),
+            indent_prefix: Some("  "),
         }
     }
 }
@@ -89,7 +89,7 @@ impl<'config, 'fmt, 'fmt2> State<'config, 'fmt, 'fmt2> {
     }
 
     fn new_line_common(&mut self, space: bool) -> fmt::Result {
-        if let Some(prefix) = self.config.ident_prefix {
+        if let Some(prefix) = self.config.indent_prefix {
             self.write("\n")?;
             for _ in 0..self.indent_level {
                 self.write(prefix)?;
@@ -100,14 +100,27 @@ impl<'config, 'fmt, 'fmt2> State<'config, 'fmt, 'fmt2> {
         Ok(())
     }
 
+    /// Panics if newlines are disabled
+    fn require_new_line(&mut self) -> fmt::Result {
+        let prefix = self
+            .config
+            .indent_prefix
+            .expect("require_new_line called with newlines disabled");
+        self.write("\n")?;
+        for _ in 0..self.indent_level {
+            self.write(prefix)?;
+        }
+        Ok(())
+    }
+
     pub(crate) fn newlines_enabled(&self) -> bool {
-        self.config.ident_prefix.is_some()
+        self.config.indent_prefix.is_some()
     }
 
     pub(crate) fn on_single_line<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
-        let indent_prefix = self.config.ident_prefix.take();
+        let indent_prefix = self.config.indent_prefix.take();
         let result = f(self);
-        self.config.ident_prefix = indent_prefix;
+        self.config.indent_prefix = indent_prefix;
         result
     }
 }
@@ -702,7 +715,10 @@ impl Value {
             Value::Boolean(true) => state.write("true"),
             Value::Boolean(false) => state.write("false"),
             Value::Enum(name) => state.write(name),
-            Value::String(value) => serialize_string_value(state, value),
+            Value::String(value) => {
+                let is_description = false;
+                serialize_string_value(state, is_description, value)
+            }
             Value::Variable(name) => display!(state, "${}", name),
             Value::Float(value) => display!(state, value),
             Value::Int(value) => display!(state, value),
@@ -800,12 +816,12 @@ pub(crate) fn curly_brackets_space_separated<T>(
     state.write("}")
 }
 
-fn serialize_string_value(state: &mut State, mut str: &str) -> fmt::Result {
-    // TODO: use block string when it would be possible AND would look nicer
-    // The parsed value of a block string cannot have:
-    // * non-\n control characters (not representable)
-    // * leading/trailing empty lines (would be stripped when reparsing)
-    // * common indent (would be stripped when reparsing)
+fn serialize_string_value(state: &mut State, is_description: bool, mut str: &str) -> fmt::Result {
+    let contains_newline = str.contains('\n');
+    let prefer_block_string = is_description || contains_newline;
+    if state.newlines_enabled() && prefer_block_string && can_be_block_string(str) {
+        return serialize_block_string(state, contains_newline, str);
+    }
     state.write("\"")?;
     loop {
         if let Some(i) = str.find(|c| (c < ' ' && c != '\t') || c == '"' || c == '\\') {
@@ -831,9 +847,100 @@ fn serialize_string_value(state: &mut State, mut str: &str) -> fmt::Result {
     state.write("\"")
 }
 
+fn serialize_block_string(state: &mut State, contains_newline: bool, str: &str) -> fmt::Result {
+    const TRIPLE_QUOTE: &str = "\"\"\"";
+    const ESCAPED_TRIPLE_QUOTE: &str = "\\\"\"\"";
+    const _: () = assert!(TRIPLE_QUOTE.len() == 3);
+    const _: () = assert!(ESCAPED_TRIPLE_QUOTE.len() == 4);
+
+    fn serialize_line(state: &mut State, mut line: &str) -> Result<(), fmt::Error> {
+        while let Some((before, after)) = line.split_once(TRIPLE_QUOTE) {
+            state.write(before)?;
+            state.write(ESCAPED_TRIPLE_QUOTE)?;
+            line = after;
+        }
+        state.write(line)
+    }
+
+    let multi_line =
+        contains_newline || str.len() > 70 || str.ends_with('"') || str.ends_with('\\');
+
+    state.write(TRIPLE_QUOTE)?;
+    if !multi_line {
+        // """example""""
+        serialize_line(state, str)?
+    } else {
+        // """
+        // example
+        // """
+
+        // `can_be_block_string` excludes \r, so the only remaining line terminator is \n
+        for line in str.split('\n') {
+            if line.is_empty() {
+                // Skip indentation which would be trailing whitespace
+                state.write("\n")?;
+            } else {
+                state.require_new_line()?;
+                serialize_line(state, line)?;
+            }
+        }
+        state.require_new_line()?;
+    }
+    state.write(TRIPLE_QUOTE)
+}
+
+/// Is it possible to create a serialization that, when fed through
+/// [BlockStringValue](https://spec.graphql.org/October2021/#BlockStringValue()),
+/// returns exactly `value`?
+fn can_be_block_string(value: &str) -> bool {
+    // `BlockStringValue` splits its inputs at any `LineTerminator` (\n, \r\n, or \r)
+    // and eventually joins lines but always with \n. So its output can never contain \r
+    if value.contains('\r') {
+        return false;
+    }
+
+    /// <https://spec.graphql.org/October2021/#WhiteSpace>
+    fn trim_start_graphql_whitespace(value: &str) -> &str {
+        value.trim_start_matches([' ', '\t'])
+    }
+
+    // With the above, \n is the only remaining LineTerminator
+    let mut lines = value.split('\n');
+    if lines
+        .next()
+        .is_some_and(|first| trim_start_graphql_whitespace(first).is_empty())
+        || lines
+            .next_back()
+            .is_some_and(|last| trim_start_graphql_whitespace(last).is_empty())
+    {
+        // Leading or trailing whitespace-only line would be trimmed by `BlockStringValue`
+        return false;
+    }
+
+    let common_indent = {
+        let mut lines = value.split('\n');
+        // Skip the first line, the one following """
+        lines.next();
+
+        let each_line_indent_utf8_len = lines.filter_map(|line| {
+            let after_indent = trim_start_graphql_whitespace(line);
+            if !after_indent.is_empty() {
+                Some(line.len() - after_indent.len())
+            } else {
+                None // skip whitespace-only lines
+            }
+        });
+        each_line_indent_utf8_len.min().unwrap_or(0)
+    };
+    // If there is common indent `BlockStringValue` would remove it
+    // and incorrectly round-trip to a different value.
+    common_indent == 0
+}
+
 fn serialize_description(state: &mut State, description: &Option<NodeStr>) -> fmt::Result {
     if let Some(description) = description {
-        serialize_string_value(state, description)?;
+        let is_description = true;
+        serialize_string_value(state, is_description, description)?;
         state.new_line_or_space()?;
     }
     Ok(())
diff --git a/crates/apollo-compiler/src/diagnostics.rs b/crates/apollo-compiler/src/diagnostics.rs
index 67ff0c3..2023606 100644
--- a/crates/apollo-compiler/src/diagnostics.rs
+++ b/crates/apollo-compiler/src/diagnostics.rs
@@ -129,6 +129,8 @@ pub(crate) enum DiagnosticData {
     RecursiveInputObjectDefinition { name: String },
     #[error("`{name}` fragment cannot reference itself")]
     RecursiveFragmentDefinition { name: String },
+    #[error("`{name}` contains too much nesting")]
+    DeeplyNestedType { name: String },
     #[error("type does not satisfy interface `{interface}`: missing field `{field}`")]
     MissingInterfaceField { interface: String, field: String },
     #[error("the required argument `{name}` is not provided")]
@@ -238,6 +240,8 @@ pub(crate) enum DiagnosticData {
         /// Name of the argument where variable is used
         arg_name: String,
     },
+    #[error("too much recursion")]
+    RecursionError {},
 }
 
 impl ApolloDiagnostic {
diff --git a/crates/apollo-compiler/src/validation/directive.rs b/crates/apollo-compiler/src/validation/directive.rs
index f7a617f..3386b54 100644
--- a/crates/apollo-compiler/src/validation/directive.rs
+++ b/crates/apollo-compiler/src/validation/directive.rs
@@ -3,6 +3,8 @@ use crate::validation::{NodeLocation, RecursionGuard, RecursionStack};
 use crate::{ast, schema, Node, ValidationDatabase};
 use std::collections::{HashMap, HashSet};
 
+use super::CycleError;
+
 /// This struct just groups functions that are used to find self-referential directives.
 /// The way to use it is to call `FindRecursiveDirective::check`.
 struct FindRecursiveDirective<'s> {
@@ -14,7 +16,7 @@ impl FindRecursiveDirective<'_> {
         &self,
         seen: &mut RecursionGuard<'_>,
         def: &schema::ExtendedType,
-    ) -> Result<(), Node<ast::Directive>> {
+    ) -> Result<(), CycleError<ast::Directive>> {
         match def {
             schema::ExtendedType::Scalar(scalar_type_definition) => {
                 self.directives(seen, &scalar_type_definition.directives)?;
@@ -49,7 +51,7 @@ impl FindRecursiveDirective<'_> {
         &self,
         seen: &mut RecursionGuard<'_>,
         input_value: &Node<ast::InputValueDefinition>,
-    ) -> Result<(), Node<ast::Directive>> {
+    ) -> Result<(), CycleError<ast::Directive>> {
         for directive in &input_value.directives {
             self.directive(seen, directive)?;
         }
@@ -66,7 +68,7 @@ impl FindRecursiveDirective<'_> {
         &self,
         seen: &mut RecursionGuard<'_>,
         enum_value: &Node<ast::EnumValueDefinition>,
-    ) -> Result<(), Node<ast::Directive>> {
+    ) -> Result<(), CycleError<ast::Directive>> {
         for directive in &enum_value.directives {
             self.directive(seen, directive)?;
         }
@@ -78,7 +80,7 @@ impl FindRecursiveDirective<'_> {
         &self,
         seen: &mut RecursionGuard<'_>,
         directives: &[schema::Component<ast::Directive>],
-    ) -> Result<(), Node<ast::Directive>> {
+    ) -> Result<(), CycleError<ast::Directive>> {
         for directive in directives {
             self.directive(seen, directive)?;
         }
@@ -89,17 +91,18 @@ impl FindRecursiveDirective<'_> {
         &self,
         seen: &mut RecursionGuard<'_>,
         directive: &Node<ast::Directive>,
-    ) -> Result<(), Node<ast::Directive>> {
+    ) -> Result<(), CycleError<ast::Directive>> {
         if !seen.contains(&directive.name) {
             if let Some(def) = self.schema.directive_definitions.get(&directive.name) {
-                self.directive_definition(seen.push(&directive.name), def)?;
+                self.directive_definition(seen.push(&directive.name)?, def)
+                    .map_err(|error| error.trace(directive))?;
             }
         } else if seen.first() == Some(&directive.name) {
             // Only report an error & bail out early if this is the *initial* directive.
             // This prevents raising confusing errors when a directive `@b` which is not
             // self-referential uses a directive `@a` that *is*. The error with `@a` should
             // only be reported on its definition, not on `@b`'s.
-            return Err(directive.clone());
+            return Err(CycleError::Recursed(vec![directive.clone()]));
         }
 
         Ok(())
@@ -109,7 +112,7 @@ impl FindRecursiveDirective<'_> {
         &self,
         mut seen: RecursionGuard<'_>,
         def: &Node<ast::DirectiveDefinition>,
-    ) -> Result<(), Node<ast::Directive>> {
+    ) -> Result<(), CycleError<ast::Directive>> {
         for input_value in &def.arguments {
             self.input_value(&mut seen, input_value)?;
         }
@@ -120,7 +123,7 @@ impl FindRecursiveDirective<'_> {
     fn check(
         schema: &schema::Schema,
         directive_def: &Node<ast::DirectiveDefinition>,
-    ) -> Result<(), Node<ast::Directive>> {
+    ) -> Result<(), CycleError<ast::Directive>> {
         let mut recursion_stack = RecursionStack::with_root(directive_def.name.clone());
         FindRecursiveDirective { schema }
             .directive_definition(recursion_stack.guard(), directive_def)
@@ -143,22 +146,59 @@ pub(crate) fn validate_directive_definition(
     // references itself directly.
     //
     // Returns Recursive Definition error.
-    if let Err(directive) = FindRecursiveDirective::check(&db.schema(), &def) {
-        let definition_location = def.location();
-        let head_location = NodeLocation::recompose(def.location(), def.name.location());
-        let directive_location = directive.location();
-
-        diagnostics.push(
-            ApolloDiagnostic::new(
+    match FindRecursiveDirective::check(&db.schema(), &def) {
+        Ok(_) => {}
+        Err(CycleError::Recursed(trace)) => {
+            let definition_location = def.location();
+            let head_location = NodeLocation::recompose(def.location(), def.name.location());
+            let mut diagnostic = ApolloDiagnostic::new(
                 db,
                 definition_location,
                 DiagnosticData::RecursiveDirectiveDefinition {
                     name: def.name.to_string(),
                 },
             )
-            .label(Label::new(head_location, "recursive directive definition"))
-            .label(Label::new(directive_location, "refers to itself here")),
-        );
+            .label(Label::new(head_location, "recursive directive definition"));
+
+            if let Some((cyclical_application, path)) = trace.split_first() {
+                let mut prev_name = &def.name;
+                for directive_application in path.iter().rev() {
+                    diagnostic = diagnostic.label(Label::new(
+                        directive_application.location(),
+                        format!(
+                            "`{}` references `{}` here...",
+                            prev_name, directive_application.name
+                        ),
+                    ));
+                    prev_name = &directive_application.name;
+                }
+
+                diagnostic = diagnostic.label(Label::new(
+                    cyclical_application.location(),
+                    format!(
+                        "`{}` circularly references `{}` here",
+                        prev_name, cyclical_application.name
+                    ),
+                ));
+            }
+
+            diagnostics.push(diagnostic);
+        }
+        Err(CycleError::Limit(_)) => {
+            let diagnostic = ApolloDiagnostic::new(
+                db,
+                def.location(),
+                DiagnosticData::DeeplyNestedType {
+                    name: def.name.to_string(),
+                },
+            )
+            .label(Label::new(
+                def.location(),
+                "directive references a very long chain of directives",
+            ));
+
+            diagnostics.push(diagnostic);
+        }
     }
 
     diagnostics
diff --git a/crates/apollo-compiler/src/validation/fragment.rs b/crates/apollo-compiler/src/validation/fragment.rs
index 1786425..ec12375 100644
--- a/crates/apollo-compiler/src/validation/fragment.rs
+++ b/crates/apollo-compiler/src/validation/fragment.rs
@@ -1,10 +1,9 @@
 use crate::diagnostics::{ApolloDiagnostic, DiagnosticData, Label};
-use crate::validation::{FileId, NodeLocation, RecursionGuard, RecursionStack};
+use crate::validation::operation::OperationValidationConfig;
+use crate::validation::{CycleError, FileId, NodeLocation, RecursionGuard, RecursionStack};
 use crate::{ast, schema, Node, ValidationDatabase};
 use std::collections::{HashMap, HashSet};
 
-use super::operation::OperationValidationConfig;
-
 /// Given a type definition, find all the type names that can be used for fragment spreading.
 ///
 /// Spec: https://spec.graphql.org/October2021/#GetPossibleTypes()
@@ -300,13 +299,13 @@ pub(crate) fn validate_fragment_cycles(
         named_fragments: &HashMap<ast::Name, Node<ast::FragmentDefinition>>,
         selection_set: &[ast::Selection],
         visited: &mut RecursionGuard<'_>,
-    ) -> Result<(), Vec<Node<ast::FragmentSpread>>> {
+    ) -> Result<(), CycleError<ast::FragmentSpread>> {
         for selection in selection_set {
             match selection {
                 ast::Selection::FragmentSpread(spread) => {
                     if visited.contains(&spread.fragment_name) {
                         if visited.first() == Some(&spread.fragment_name) {
-                            return Err(vec![spread.clone()]);
+                            return Err(CycleError::Recursed(vec![spread.clone()]));
                         }
                         continue;
                     }
@@ -315,12 +314,9 @@ pub(crate) fn validate_fragment_cycles(
                         detect_fragment_cycles(
                             named_fragments,
                             &fragment.selection_set,
-                            &mut visited.push(&fragment.name),
+                            &mut visited.push(&fragment.name)?,
                         )
-                        .map_err(|mut trace| {
-                            trace.push(spread.clone());
-                            trace
-                        })?;
+                        .map_err(|error| error.trace(spread))?;
                     }
                 }
                 ast::Selection::InlineFragment(inline) => {
@@ -335,45 +331,63 @@ pub(crate) fn validate_fragment_cycles(
         Ok(())
     }
 
-    let mut visited = RecursionStack::with_root(def.name.clone());
+    let mut visited = RecursionStack::with_root(def.name.clone()).with_limit(100);
 
-    if let Err(cycle) =
-        detect_fragment_cycles(&named_fragments, &def.selection_set, &mut visited.guard())
-    {
-        let head_location = NodeLocation::recompose(def.location(), def.name.location());
+    match detect_fragment_cycles(&named_fragments, &def.selection_set, &mut visited.guard()) {
+        Ok(_) => {}
+        Err(CycleError::Recursed(trace)) => {
+            let head_location = NodeLocation::recompose(def.location(), def.name.location());
 
-        let mut diagnostic = ApolloDiagnostic::new(
-            db,
-            def.location(),
-            DiagnosticData::RecursiveFragmentDefinition {
-                name: def.name.to_string(),
-            },
-        )
-        .label(Label::new(head_location, "recursive fragment definition"));
+            let mut diagnostic = ApolloDiagnostic::new(
+                db,
+                def.location(),
+                DiagnosticData::RecursiveFragmentDefinition {
+                    name: def.name.to_string(),
+                },
+            )
+            .label(Label::new(head_location, "recursive fragment definition"));
+
+            if let Some((cyclical_spread, path)) = trace.split_first() {
+                let mut prev_name = &def.name;
+                for spread in path.iter().rev() {
+                    diagnostic = diagnostic.label(Label::new(
+                        spread.location(),
+                        format!(
+                            "`{}` references `{}` here...",
+                            prev_name, spread.fragment_name
+                        ),
+                    ));
+                    prev_name = &spread.fragment_name;
+                }
 
-        if let Some((cyclical_spread, path)) = cycle.split_first() {
-            let mut prev_name = &def.name;
-            for spread in path.iter().rev() {
                 diagnostic = diagnostic.label(Label::new(
-                    spread.location(),
+                    cyclical_spread.location(),
                     format!(
-                        "`{}` references `{}` here...",
-                        prev_name, spread.fragment_name
+                        "`{}` circularly references `{}` here",
+                        prev_name, cyclical_spread.fragment_name
                     ),
                 ));
-                prev_name = &spread.fragment_name;
             }
 
-            diagnostic = diagnostic.label(Label::new(
-                cyclical_spread.location(),
-                format!(
-                    "`{}` circularly references `{}` here",
-                    prev_name, cyclical_spread.fragment_name
-                ),
-            ));
+            diagnostics.push(diagnostic);
         }
+        Err(CycleError::Limit(_)) => {
+            let head_location = NodeLocation::recompose(def.location(), def.name.location());
 
-        diagnostics.push(diagnostic);
+            let diagnostic = ApolloDiagnostic::new(
+                db,
+                def.location(),
+                DiagnosticData::DeeplyNestedType {
+                    name: def.name.to_string(),
+                },
+            )
+            .label(Label::new(
+                head_location,
+                "fragment references a very long chain of fragments",
+            ));
+
+            diagnostics.push(diagnostic);
+        }
     }
 
     diagnostics
diff --git a/crates/apollo-compiler/src/validation/input_object.rs b/crates/apollo-compiler/src/validation/input_object.rs
index 361fb97..3590ec7 100644
--- a/crates/apollo-compiler/src/validation/input_object.rs
+++ b/crates/apollo-compiler/src/validation/input_object.rs
@@ -2,7 +2,7 @@ use crate::{
     ast,
     diagnostics::{ApolloDiagnostic, DiagnosticData, Label},
     schema,
-    validation::{RecursionGuard, RecursionStack},
+    validation::{CycleError, RecursionGuard, RecursionStack},
     Node, ValidationDatabase,
 };
 use std::collections::HashMap;
@@ -18,7 +18,7 @@ impl FindRecursiveInputValue<'_> {
         &self,
         seen: &mut RecursionGuard<'_>,
         def: &Node<ast::InputValueDefinition>,
-    ) -> Result<(), Node<ast::InputValueDefinition>> {
+    ) -> Result<(), CycleError<ast::InputValueDefinition>> {
         return match &*def.ty {
             // NonNull type followed by Named type is the one that's not allowed
             // to be cyclical, so this is only case we care about.
@@ -26,11 +26,12 @@ impl FindRecursiveInputValue<'_> {
             // Everything else may be a cyclical input value.
             ast::Type::NonNullNamed(name) => {
                 if !seen.contains(name) {
-                    if let Some(def) = self.db.ast_types().input_objects.get(name) {
-                        self.input_object_definition(seen.push(name), def)?
+                    if let Some(object_def) = self.db.ast_types().input_objects.get(name) {
+                        self.input_object_definition(seen.push(name)?, object_def)
+                            .map_err(|err| err.trace(def))?
                     }
                 } else if seen.first() == Some(name) {
-                    return Err(def.clone());
+                    return Err(CycleError::Recursed(vec![def.clone()]));
                 }
 
                 Ok(())
@@ -43,7 +44,7 @@ impl FindRecursiveInputValue<'_> {
         &self,
         mut seen: RecursionGuard<'_>,
         input_object: &ast::TypeWithExtensions<ast::InputObjectTypeDefinition>,
-    ) -> Result<(), Node<ast::InputValueDefinition>> {
+    ) -> Result<(), CycleError<ast::InputValueDefinition>> {
         for input_value in input_object.fields() {
             self.input_value_definition(&mut seen, input_value)?;
         }
@@ -54,7 +55,7 @@ impl FindRecursiveInputValue<'_> {
     fn check(
         db: &dyn ValidationDatabase,
         input_object: &ast::TypeWithExtensions<ast::InputObjectTypeDefinition>,
-    ) -> Result<(), Node<ast::InputValueDefinition>> {
+    ) -> Result<(), CycleError<ast::InputValueDefinition>> {
         let mut recursion_stack = RecursionStack::with_root(input_object.definition.name.clone());
         FindRecursiveInputValue { db }
             .input_object_definition(recursion_stack.guard(), input_object)
@@ -85,23 +86,56 @@ pub(crate) fn validate_input_object_definition(
         Default::default(),
     );
 
-    if let Err(input_val) = FindRecursiveInputValue::check(db, &input_object) {
-        let mut labels = vec![Label::new(
-            input_object.definition.location(),
-            "cyclical input object definition",
-        )];
-        let loc = input_val.location();
-        labels.push(Label::new(loc, "refers to itself here"));
-        diagnostics.push(
-            ApolloDiagnostic::new(
+    match FindRecursiveInputValue::check(db, &input_object) {
+        Ok(_) => {}
+        Err(CycleError::Recursed(trace)) => {
+            let mut diagnostic = ApolloDiagnostic::new(
                 db,
                 input_object.definition.location(),
                 DiagnosticData::RecursiveInputObjectDefinition {
                     name: input_object.definition.name.to_string(),
                 },
             )
-            .labels(labels),
-        )
+            .label(Label::new(
+                input_object.definition.location(),
+                "cyclical input object definition",
+            ));
+
+            if let Some((cyclical_reference, path)) = trace.split_first() {
+                let mut prev_name = &input_object.definition.name;
+                for reference in path.iter().rev() {
+                    diagnostic = diagnostic.label(Label::new(
+                        reference.location(),
+                        format!("`{}` references `{}` here...", prev_name, reference.name),
+                    ));
+                    prev_name = &reference.name;
+                }
+
+                diagnostic = diagnostic.label(Label::new(
+                    cyclical_reference.location(),
+                    format!(
+                        "`{}` circularly references `{}` here",
+                        prev_name, cyclical_reference.name
+                    ),
+                ));
+            }
+            diagnostics.push(diagnostic);
+        }
+        Err(CycleError::Limit(_)) => {
+            let diagnostic = ApolloDiagnostic::new(
+                db,
+                input_object.definition.location(),
+                DiagnosticData::DeeplyNestedType {
+                    name: input_object.definition.name.to_string(),
+                },
+            )
+            .label(Label::new(
+                input_object.definition.location(),
+                "input object references a very long chain of input objects",
+            ));
+
+            diagnostics.push(diagnostic);
+        }
     }
 
     // Fields in an Input Object Definition must be unique
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index b32b2ba..ceff5a0 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -25,6 +25,7 @@ use crate::ast::Name;
 use crate::executable::BuildError as ExecutableBuildError;
 use crate::execution::{GraphQLError, GraphQLLocation};
 use crate::schema::BuildError as SchemaBuildError;
+use crate::Node;
 use crate::SourceFile;
 use crate::SourceMap;
 use indexmap::IndexSet;
@@ -565,21 +566,44 @@ impl From<ExecutableBuildError> for Details {
     }
 }
 
+const DEFAULT_RECURSION_LIMIT: usize = 32;
+
+#[derive(Debug, Clone, thiserror::Error)]
+#[error("Recursion limit reached")]
+#[non_exhaustive]
+struct RecursionLimitError {}
+
 /// Track used names in a recursive function.
+#[derive(Debug)]
 struct RecursionStack {
     seen: IndexSet<Name>,
+    high: usize,
+    limit: usize,
 }
 
 impl RecursionStack {
+    fn new() -> Self {
+        Self {
+            seen: IndexSet::new(),
+            high: 0,
+            limit: DEFAULT_RECURSION_LIMIT,
+        }
+    }
+
     fn with_root(root: Name) -> Self {
-        let mut seen = IndexSet::new();
-        seen.insert(root);
-        Self { seen }
+        let mut stack = Self::new();
+        stack.seen.insert(root);
+        stack
+    }
+
+    fn with_limit(mut self, limit: usize) -> Self {
+        self.limit = limit;
+        self
     }
 
     /// Return the actual API for tracking recursive uses.
     pub(crate) fn guard(&mut self) -> RecursionGuard<'_> {
-        RecursionGuard(&mut self.seen)
+        RecursionGuard(self)
     }
 }
 
@@ -588,29 +612,55 @@ impl RecursionStack {
 /// Pass the result of `guard.push(name)` to recursive calls. Use `guard.contains(name)` to check
 /// if the name was used somewhere up the call stack. When a guard is dropped, its name is removed
 /// from the list.
-struct RecursionGuard<'a>(&'a mut IndexSet<Name>);
+struct RecursionGuard<'a>(&'a mut RecursionStack);
 impl RecursionGuard<'_> {
-    /// Mark that we saw a name.
-    fn push(&mut self, name: &Name) -> RecursionGuard<'_> {
+    /// Mark that we saw a name. If there are too many names, return an error.
+    fn push(&mut self, name: &Name) -> Result<RecursionGuard<'_>, RecursionLimitError> {
         debug_assert!(
-            self.0.insert(name.clone()),
+            self.0.seen.insert(name.clone()),
             "cannot push the same name twice to RecursionGuard, check contains() first"
         );
-        RecursionGuard(self.0)
+        self.0.high = self.0.high.max(self.0.seen.len());
+        if self.0.seen.len() > self.0.limit {
+            Err(RecursionLimitError {})
+        } else {
+            Ok(RecursionGuard(self.0))
+        }
     }
     /// Check if we saw a name somewhere up the call stack.
     fn contains(&self, name: &Name) -> bool {
-        self.0.iter().any(|seen| seen == name)
+        self.0.seen.iter().any(|seen| seen == name)
     }
     /// Return the name where we started.
     fn first(&self) -> Option<&Name> {
-        self.0.first()
+        self.0.seen.first()
     }
 }
 
 impl Drop for RecursionGuard<'_> {
     fn drop(&mut self) {
         // This may already be empty if it's the original `stack.guard()` result, but that's fine
-        self.0.pop();
+        let _ = self.0.seen.pop();
+    }
+}
+
+/// Errors that can happen when chasing potentially cyclical references.
+#[derive(Debug, Clone, thiserror::Error)]
+enum CycleError<T> {
+    /// Detected a cycle, value contains the path from the offending node back to the node where we
+    /// started.
+    #[error("Cycle detected")]
+    Recursed(Vec<Node<T>>),
+    /// Ran into recursion limit before a cycle could be detected.
+    #[error(transparent)]
+    Limit(#[from] RecursionLimitError),
+}
+
+impl<T> CycleError<T> {
+    fn trace(mut self, node: &Node<T>) -> Self {
+        if let Self::Recursed(trace) = &mut self {
+            trace.push(node.clone());
+        }
+        self
     }
 }
diff --git a/crates/apollo-compiler/src/validation/selection.rs b/crates/apollo-compiler/src/validation/selection.rs
index c01c14b..3667584 100644
--- a/crates/apollo-compiler/src/validation/selection.rs
+++ b/crates/apollo-compiler/src/validation/selection.rs
@@ -176,15 +176,25 @@ pub(crate) fn same_response_shape(
         }
         // 6. Assert: typeA and typeB are both composite types.
         (def_a, def_b) if is_composite(def_a) && is_composite(def_b) => {
+            let Ok(subfield_a_type) = schema.type_field(field_a.against_type, &field_a.field.name)
+            else {
+                // Missing field: error raised elsewhere, we can't check if the type is correct
+                return Ok(());
+            };
+            let Ok(subfield_b_type) = schema.type_field(field_b.against_type, &field_b.field.name)
+            else {
+                // Missing field: error raised elsewhere, we can't check if the type is correct
+                return Ok(());
+            };
             let named_fragments = db.ast_named_fragments(file_id);
             let mut merged_set = operation_fields(
                 &named_fragments,
-                field_a.against_type,
+                &subfield_a_type.name,
                 &field_a.field.selection_set,
             );
             merged_set.extend(operation_fields(
                 &named_fragments,
-                field_b.against_type,
+                &subfield_b_type.name,
                 &field_b.field.selection_set,
             ));
             let grouped_by_name = group_fields_by_name(merged_set);
diff --git a/crates/apollo-compiler/src/validation/variable.rs b/crates/apollo-compiler/src/validation/variable.rs
index 911af13..eb05aa7 100644
--- a/crates/apollo-compiler/src/validation/variable.rs
+++ b/crates/apollo-compiler/src/validation/variable.rs
@@ -1,5 +1,7 @@
 use crate::diagnostics::{ApolloDiagnostic, DiagnosticData, Label};
-use crate::validation::{FileId, NodeLocation};
+use crate::validation::{
+    FileId, NodeLocation, RecursionGuard, RecursionLimitError, RecursionStack,
+};
 use crate::{ast, schema, Node, ValidationDatabase};
 use std::collections::hash_map::Entry;
 use std::collections::{HashMap, HashSet};
@@ -105,16 +107,11 @@ pub(crate) fn validate_variable_definitions2(
     diagnostics
 }
 
-enum SelectionPathElement<'ast> {
-    Field(&'ast ast::Field),
-    Fragment(&'ast ast::FragmentSpread),
-    InlineFragment(&'ast ast::InlineFragment),
-}
 fn walk_selections(
     document: &ast::Document,
     selections: &[ast::Selection],
     mut f: impl FnMut(&ast::Selection),
-) {
+) -> Result<(), RecursionLimitError> {
     type NamedFragments = HashMap<ast::Name, Node<ast::FragmentDefinition>>;
     let named_fragments: NamedFragments = document
         .definitions
@@ -127,55 +124,48 @@ fn walk_selections(
         })
         .collect();
 
-    fn walk_selections_inner<'ast: 'path, 'path>(
+    fn walk_selections_inner<'ast, 'guard>(
         named_fragments: &'ast NamedFragments,
         selections: &'ast [ast::Selection],
-        path: &mut Vec<SelectionPathElement<'path>>,
+        guard: &mut RecursionGuard<'guard>,
         f: &mut dyn FnMut(&ast::Selection),
-    ) {
+    ) -> Result<(), RecursionLimitError> {
         for selection in selections {
             f(selection);
             match selection {
                 ast::Selection::Field(field) => {
-                    path.push(SelectionPathElement::Field(field));
-                    walk_selections_inner(named_fragments, &field.selection_set, path, f);
-                    path.pop();
+                    walk_selections_inner(named_fragments, &field.selection_set, guard, f)?;
                 }
                 ast::Selection::FragmentSpread(fragment) => {
-                    // prevent recursion
-                    if path.iter().any(|element| {
-                        matches!(element, SelectionPathElement::Fragment(walked) if walked.fragment_name == fragment.fragment_name)
-                    }) {
+                    // Prevent chasing a cyclical reference.
+                    // Note we do not report `CycleError::Recursed` here, as that is already caught
+                    // by the cyclical fragment validation--we just need to ensure that we don't
+                    // overflow the stack.
+                    if guard.contains(&fragment.fragment_name) {
                         continue;
                     }
 
-                    path.push(SelectionPathElement::Fragment(fragment));
                     if let Some(fragment_definition) = named_fragments.get(&fragment.fragment_name)
                     {
                         walk_selections_inner(
                             named_fragments,
                             &fragment_definition.selection_set,
-                            path,
+                            &mut guard.push(&fragment.fragment_name)?,
                             f,
-                        );
+                        )?;
                     }
-                    path.pop();
                 }
                 ast::Selection::InlineFragment(fragment) => {
-                    path.push(SelectionPathElement::InlineFragment(fragment));
-                    walk_selections_inner(named_fragments, &fragment.selection_set, path, f);
-                    path.pop();
+                    walk_selections_inner(named_fragments, &fragment.selection_set, guard, f)?;
                 }
             }
         }
+        Ok(())
     }
 
-    walk_selections_inner(
-        &named_fragments,
-        selections,
-        &mut Default::default(),
-        &mut f,
-    );
+    let mut stack = RecursionStack::new().with_limit(100);
+    let result = walk_selections_inner(&named_fragments, selections, &mut stack.guard(), &mut f);
+    result
 }
 
 fn variables_in_value(value: &ast::Value) -> impl Iterator<Item = ast::Name> + '_ {
@@ -218,6 +208,8 @@ pub(crate) fn validate_unused_variables(
     file_id: FileId,
     operation: Node<ast::OperationDefinition>,
 ) -> Vec<ApolloDiagnostic> {
+    let mut diagnostics = Vec::new();
+
     let defined_vars: HashSet<_> = operation
         .variables
         .iter()
@@ -234,24 +226,31 @@ pub(crate) fn validate_unused_variables(
         })
         .collect();
     let mut used_vars = HashSet::<ast::Name>::new();
-    walk_selections(
-        &db.ast(file_id),
-        &operation.selection_set,
-        |selection| match selection {
-            ast::Selection::Field(field) => {
-                used_vars.extend(variables_in_directives(&field.directives));
-                used_vars.extend(variables_in_arguments(&field.arguments));
-            }
-            ast::Selection::FragmentSpread(fragment) => {
-                used_vars.extend(variables_in_directives(&fragment.directives));
-            }
-            ast::Selection::InlineFragment(fragment) => {
-                used_vars.extend(variables_in_directives(&fragment.directives));
-            }
-        },
-    );
-
-    let mut diagnostics = Vec::new();
+    let walked =
+        walk_selections(
+            &db.ast(file_id),
+            &operation.selection_set,
+            |selection| match selection {
+                ast::Selection::Field(field) => {
+                    used_vars.extend(variables_in_directives(&field.directives));
+                    used_vars.extend(variables_in_arguments(&field.arguments));
+                }
+                ast::Selection::FragmentSpread(fragment) => {
+                    used_vars.extend(variables_in_directives(&fragment.directives));
+                }
+                ast::Selection::InlineFragment(fragment) => {
+                    used_vars.extend(variables_in_directives(&fragment.directives));
+                }
+            },
+        );
+    if walked.is_err() {
+        diagnostics.push(ApolloDiagnostic::new(
+            db,
+            None,
+            DiagnosticData::RecursionError {},
+        ));
+        return diagnostics;
+    }
 
     let unused_vars = defined_vars.difference(&used_vars);
 


Test patch:
diff --git a/crates/apollo-compiler/test_data/diagnostics/0070_self_referential_directive_definition.txt b/crates/apollo-compiler/test_data/diagnostics/0070_self_referential_directive_definition.txt
index 1f0d8cf..c83967b 100644
--- a/crates/apollo-compiler/test_data/diagnostics/0070_self_referential_directive_definition.txt
+++ b/crates/apollo-compiler/test_data/diagnostics/0070_self_referential_directive_definition.txt
@@ -5,14 +5,14 @@ Error: `invalidExample` directive definition cannot reference itself
    │ ────────────┬────────────             ───────┬───────  
    │             ╰────────────────────────────────────────── recursive directive definition
    │                                              │         
-   │                                              ╰───────── refers to itself here
+   │                                              ╰───────── `invalidExample` circularly references `invalidExample` here
 ───╯
 Error: `deprecatedType` directive definition cannot reference itself
     ╭─[0070_self_referential_directive_definition.graphql:11:1]
     │
  10 │ extend scalar String @deprecatedType(reason: "use OurCustomString instead")
     │                      ───────────────────────────┬──────────────────────────  
-    │                                                 ╰──────────────────────────── refers to itself here
+    │                                                 ╰──────────────────────────── `deprecatedType` circularly references `deprecatedType` here
  11 │ directive @deprecatedType(reason: String!) on OBJECT | INTERFACE | ENUM | SCALAR | UNION
     │ ────────────┬────────────  
     │             ╰────────────── recursive directive definition
@@ -21,39 +21,53 @@ Error: `loopA` directive definition cannot reference itself
     ╭─[0070_self_referential_directive_definition.graphql:13:1]
     │
  13 │ directive @loopA(arg: Boolean @loopB) on ARGUMENT_DEFINITION
-    │ ────────┬───────  
-    │         ╰───────── recursive directive definition
-    │ 
+    │ ────────┬───────              ───┬──  
+    │         ╰───────────────────────────── recursive directive definition
+    │                                  │    
+    │                                  ╰──── `loopA` references `loopB` here...
+ 14 │ directive @loopB(arg: Boolean @loopC) on ARGUMENT_DEFINITION
+    │                               ───┬──  
+    │                                  ╰──── `loopB` references `loopC` here...
  15 │ directive @loopC(arg: Boolean @loopA) on ARGUMENT_DEFINITION
     │                               ───┬──  
-    │                                  ╰──── refers to itself here
+    │                                  ╰──── `loopC` circularly references `loopA` here
 ────╯
 Error: `loopB` directive definition cannot reference itself
     ╭─[0070_self_referential_directive_definition.graphql:14:1]
     │
  13 │ directive @loopA(arg: Boolean @loopB) on ARGUMENT_DEFINITION
     │                               ───┬──  
-    │                                  ╰──── refers to itself here
+    │                                  ╰──── `loopA` circularly references `loopB` here
  14 │ directive @loopB(arg: Boolean @loopC) on ARGUMENT_DEFINITION
-    │ ────────┬───────  
-    │         ╰───────── recursive directive definition
+    │ ────────┬───────              ───┬──  
+    │         ╰───────────────────────────── recursive directive definition
+    │                                  │    
+    │                                  ╰──── `loopB` references `loopC` here...
+ 15 │ directive @loopC(arg: Boolean @loopA) on ARGUMENT_DEFINITION
+    │                               ───┬──  
+    │                                  ╰──── `loopC` references `loopA` here...
 ────╯
 Error: `loopC` directive definition cannot reference itself
     ╭─[0070_self_referential_directive_definition.graphql:15:1]
     │
+ 13 │ directive @loopA(arg: Boolean @loopB) on ARGUMENT_DEFINITION
+    │                               ───┬──  
+    │                                  ╰──── `loopA` references `loopB` here...
  14 │ directive @loopB(arg: Boolean @loopC) on ARGUMENT_DEFINITION
     │                               ───┬──  
-    │                                  ╰──── refers to itself here
+    │                                  ╰──── `loopB` circularly references `loopC` here
  15 │ directive @loopC(arg: Boolean @loopA) on ARGUMENT_DEFINITION
-    │ ────────┬───────  
-    │         ╰───────── recursive directive definition
+    │ ────────┬───────              ───┬──  
+    │         ╰───────────────────────────── recursive directive definition
+    │                                  │    
+    │                                  ╰──── `loopC` references `loopA` here...
 ────╯
 Error: `wrong` directive definition cannot reference itself
     ╭─[0070_self_referential_directive_definition.graphql:21:1]
     │
  18 │ extend enum Enum { RECURSIVE @wrong }
     │                              ───┬──  
-    │                                 ╰──── refers to itself here
+    │                                 ╰──── `wrong` circularly references `wrong` here
     │ 
  21 │ directive @wrong(input: InputObject) on INPUT_FIELD_DEFINITION | ENUM_VALUE
     │ ────────┬───────  
diff --git a/crates/apollo-compiler/test_data/diagnostics/0084_circular_non_nullable_input_objects.txt b/crates/apollo-compiler/test_data/diagnostics/0084_circular_non_nullable_input_objects.txt
index b7b89e6..e32b245 100644
--- a/crates/apollo-compiler/test_data/diagnostics/0084_circular_non_nullable_input_objects.txt
+++ b/crates/apollo-compiler/test_data/diagnostics/0084_circular_non_nullable_input_objects.txt
@@ -2,50 +2,93 @@ Error: `First` input object cannot reference itself
     ╭─[0084_circular_non_nullable_input_objects.graphql:6:1]
     │
   6 │ ╭─▶ input First {
+  7 │ │     second: Second!
+    │ │     ───────┬───────  
+    │ │            ╰───────── `First` references `second` here...
     ┆ ┆   
   9 │ ├─▶ }
     │ │       
     │ ╰─────── cyclical input object definition
     │ 
+ 12 │       third: Third!
+    │       ──────┬──────  
+    │             ╰──────── `second` references `third` here...
+    │ 
+ 17 │       fourth: Fourth!
+    │       ───────┬───────  
+    │              ╰───────── `third` references `fourth` here...
+    │ 
  22 │       first: First!
     │       ──────┬──────  
-    │             ╰──────── refers to itself here
+    │             ╰──────── `fourth` circularly references `first` here
 ────╯
 Error: `Second` input object cannot reference itself
     ╭─[0084_circular_non_nullable_input_objects.graphql:11:1]
     │
   7 │       second: Second!
     │       ───────┬───────  
-    │              ╰───────── refers to itself here
+    │              ╰───────── `first` circularly references `second` here
     │ 
  11 │ ╭─▶ input Second {
+ 12 │ │     third: Third!
+    │ │     ──────┬──────  
+    │ │           ╰──────── `Second` references `third` here...
     ┆ ┆   
  14 │ ├─▶ }
     │ │       
     │ ╰─────── cyclical input object definition
+    │ 
+ 17 │       fourth: Fourth!
+    │       ───────┬───────  
+    │              ╰───────── `third` references `fourth` here...
+    │ 
+ 22 │       first: First!
+    │       ──────┬──────  
+    │             ╰──────── `fourth` references `first` here...
 ────╯
 Error: `Third` input object cannot reference itself
     ╭─[0084_circular_non_nullable_input_objects.graphql:16:1]
     │
+  7 │       second: Second!
+    │       ───────┬───────  
+    │              ╰───────── `first` references `second` here...
+    │ 
  12 │       third: Third!
     │       ──────┬──────  
-    │             ╰──────── refers to itself here
+    │             ╰──────── `second` circularly references `third` here
     │ 
  16 │ ╭─▶ input Third {
+ 17 │ │     fourth: Fourth!
+    │ │     ───────┬───────  
+    │ │            ╰───────── `Third` references `fourth` here...
     ┆ ┆   
  19 │ ├─▶ }
     │ │       
     │ ╰─────── cyclical input object definition
+    │ 
+ 22 │       first: First!
+    │       ──────┬──────  
+    │             ╰──────── `fourth` references `first` here...
 ────╯
 Error: `Fourth` input object cannot reference itself
     ╭─[0084_circular_non_nullable_input_objects.graphql:21:1]
     │
+  7 │       second: Second!
+    │       ───────┬───────  
+    │              ╰───────── `first` references `second` here...
+    │ 
+ 12 │       third: Third!
+    │       ──────┬──────  
+    │             ╰──────── `second` references `third` here...
+    │ 
  17 │       fourth: Fourth!
     │       ───────┬───────  
-    │              ╰───────── refers to itself here
+    │              ╰───────── `third` circularly references `fourth` here
     │ 
  21 │ ╭─▶ input Fourth {
-    ┆ ┆   
+ 22 │ │     first: First!
+    │ │     ──────┬──────  
+    │ │           ╰──────── `Fourth` references `first` here...
  23 │ ├─▶ }
     │ │      
     │ ╰────── cyclical input object definition
diff --git a/crates/apollo-compiler/test_data/ok/0039_string_literals.graphql b/crates/apollo-compiler/test_data/ok/0039_string_literals.graphql
new file mode 100644
index 0000000..b468bc7
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0039_string_literals.graphql
@@ -0,0 +1,51 @@
+type Query {
+  "LF: a\nb"
+  f1: Int
+
+  "CRLF: a\r\nb"
+  f2: Int
+
+  """
+    a
+
+    b
+  
+
+  """
+  f3: Int
+
+  "a \"b\" c"
+  f4: Int
+
+  """a \"""b\""" c"""
+  f5: Int
+
+  """
+  regex: \d+
+  """
+  f6: Int
+
+  "\nLeading empty line to preserve"
+  f7: Int
+
+  " \nLeading whitespace-only line to preserve"
+  f8: Int
+
+  "Trailing empty line to preserve\n"
+  f9: Int
+
+  "Trailing whitespace-only line to preserve\n\t"
+  f10: Int
+
+  f11(arg: String = "a\nb"): Int
+
+  f12(arg: String = "a \"b\" c"): Int
+
+  f13(arg: String = "regex: \\d+"): Int
+
+  "Trailing backslash \\"
+  f14: Int
+
+  "Trailing quote\""
+  f15: Int
+}
diff --git a/crates/apollo-compiler/test_data/ok/0039_string_literals.txt b/crates/apollo-compiler/test_data/ok/0039_string_literals.txt
new file mode 100644
index 0000000..5fa3b13
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0039_string_literals.txt
@@ -0,0 +1,316 @@
+Schema {
+    sources: {
+        -1: SourceFile {
+            path: "built_in.graphql",
+            source_text: include_str!("built_in.graphql"),
+        },
+        38: SourceFile {
+            path: "0039_string_literals.graphql",
+            source_text: "type Query {\n  \"LF: a\\nb\"\n  f1: Int\n\n  \"CRLF: a\\r\\nb\"\n  f2: Int\n\n  \"\"\"\n    a\n\n    b\n  \n\n  \"\"\"\n  f3: Int\n\n  \"a \\\"b\\\" c\"\n  f4: Int\n\n  \"\"\"a \\\"\"\"b\\\"\"\" c\"\"\"\n  f5: Int\n\n  \"\"\"\n  regex: \\d+\n  \"\"\"\n  f6: Int\n\n  \"\\nLeading empty line to preserve\"\n  f7: Int\n\n  \" \\nLeading whitespace-only line to preserve\"\n  f8: Int\n\n  \"Trailing empty line to preserve\\n\"\n  f9: Int\n\n  \"Trailing whitespace-only line to preserve\\n\\t\"\n  f10: Int\n\n  f11(arg: String = \"a\\nb\"): Int\n\n  f12(arg: String = \"a \\\"b\\\" c\"): Int\n\n  f13(arg: String = \"regex: \\\\d+\"): Int\n\n  \"Trailing backslash \\\\\"\n  f14: Int\n\n  \"Trailing quote\\\"\"\n  f15: Int\n}\n",
+        },
+    },
+    schema_definition: SchemaDefinition {
+        description: None,
+        directives: [],
+        query: Some(
+            ComponentName {
+                origin: Definition,
+                name: "Query",
+            },
+        ),
+        mutation: None,
+        subscription: None,
+    },
+    directive_definitions: {
+        "skip": built_in_directive!("skip"),
+        "include": built_in_directive!("include"),
+        "deprecated": built_in_directive!("deprecated"),
+        "specifiedBy": built_in_directive!("specifiedBy"),
+    },
+    types: {
+        "__Schema": built_in_type!("__Schema"),
+        "__Type": built_in_type!("__Type"),
+        "__TypeKind": built_in_type!("__TypeKind"),
+        "__Field": built_in_type!("__Field"),
+        "__InputValue": built_in_type!("__InputValue"),
+        "__EnumValue": built_in_type!("__EnumValue"),
+        "__Directive": built_in_type!("__Directive"),
+        "__DirectiveLocation": built_in_type!("__DirectiveLocation"),
+        "Int": built_in_type!("Int"),
+        "Float": built_in_type!("Float"),
+        "String": built_in_type!("String"),
+        "Boolean": built_in_type!("Boolean"),
+        "ID": built_in_type!("ID"),
+        "Query": Object(
+            0..602 @38 ObjectType {
+                description: None,
+                name: "Query",
+                implements_interfaces: {},
+                directives: [],
+                fields: {
+                    "f1": Component {
+                        origin: Definition,
+                        node: 15..35 @38 FieldDefinition {
+                            description: Some(
+                                "LF: a\nb",
+                            ),
+                            name: "f1",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f2": Component {
+                        origin: Definition,
+                        node: 39..63 @38 FieldDefinition {
+                            description: Some(
+                                "CRLF: a\r\nb",
+                            ),
+                            name: "f2",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f3": Component {
+                        origin: Definition,
+                        node: 67..103 @38 FieldDefinition {
+                            description: Some(
+                                "a\n\nb",
+                            ),
+                            name: "f3",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f4": Component {
+                        origin: Definition,
+                        node: 107..128 @38 FieldDefinition {
+                            description: Some(
+                                "a \"b\" c",
+                            ),
+                            name: "f4",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f5": Component {
+                        origin: Definition,
+                        node: 132..161 @38 FieldDefinition {
+                            description: Some(
+                                "a \"\"\"b\"\"\" c",
+                            ),
+                            name: "f5",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f6": Component {
+                        origin: Definition,
+                        node: 165..197 @38 FieldDefinition {
+                            description: Some(
+                                "regex: \\d+",
+                            ),
+                            name: "f6",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f7": Component {
+                        origin: Definition,
+                        node: 201..245 @38 FieldDefinition {
+                            description: Some(
+                                "\nLeading empty line to preserve",
+                            ),
+                            name: "f7",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f8": Component {
+                        origin: Definition,
+                        node: 249..304 @38 FieldDefinition {
+                            description: Some(
+                                " \nLeading whitespace-only line to preserve",
+                            ),
+                            name: "f8",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f9": Component {
+                        origin: Definition,
+                        node: 308..353 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing empty line to preserve\n",
+                            ),
+                            name: "f9",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f10": Component {
+                        origin: Definition,
+                        node: 357..415 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing whitespace-only line to preserve\n\t",
+                            ),
+                            name: "f10",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f11": Component {
+                        origin: Definition,
+                        node: 419..449 @38 FieldDefinition {
+                            description: None,
+                            name: "f11",
+                            arguments: [
+                                423..443 @38 InputValueDefinition {
+                                    description: None,
+                                    name: "arg",
+                                    ty: 428..434 @38 Named(
+                                        "String",
+                                    ),
+                                    default_value: Some(
+                                        437..443 @38 String(
+                                            "a\nb",
+                                        ),
+                                    ),
+                                    directives: [],
+                                },
+                            ],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f12": Component {
+                        origin: Definition,
+                        node: 453..488 @38 FieldDefinition {
+                            description: None,
+                            name: "f12",
+                            arguments: [
+                                457..482 @38 InputValueDefinition {
+                                    description: None,
+                                    name: "arg",
+                                    ty: 462..468 @38 Named(
+                                        "String",
+                                    ),
+                                    default_value: Some(
+                                        471..482 @38 String(
+                                            "a \"b\" c",
+                                        ),
+                                    ),
+                                    directives: [],
+                                },
+                            ],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f13": Component {
+                        origin: Definition,
+                        node: 492..529 @38 FieldDefinition {
+                            description: None,
+                            name: "f13",
+                            arguments: [
+                                496..523 @38 InputValueDefinition {
+                                    description: None,
+                                    name: "arg",
+                                    ty: 501..507 @38 Named(
+                                        "String",
+                                    ),
+                                    default_value: Some(
+                                        510..523 @38 String(
+                                            "regex: \\d+",
+                                        ),
+                                    ),
+                                    directives: [],
+                                },
+                            ],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f14": Component {
+                        origin: Definition,
+                        node: 533..567 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing backslash \\",
+                            ),
+                            name: "f14",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "f15": Component {
+                        origin: Definition,
+                        node: 571..600 @38 FieldDefinition {
+                            description: Some(
+                                "Trailing quote\"",
+                            ),
+                            name: "f15",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                },
+            },
+        ),
+    },
+}
+ExecutableDocument {
+    sources: {
+        -1: SourceFile {
+            path: "built_in.graphql",
+            source_text: include_str!("built_in.graphql"),
+        },
+        38: SourceFile {
+            path: "0039_string_literals.graphql",
+            source_text: "type Query {\n  \"LF: a\\nb\"\n  f1: Int\n\n  \"CRLF: a\\r\\nb\"\n  f2: Int\n\n  \"\"\"\n    a\n\n    b\n  \n\n  \"\"\"\n  f3: Int\n\n  \"a \\\"b\\\" c\"\n  f4: Int\n\n  \"\"\"a \\\"\"\"b\\\"\"\" c\"\"\"\n  f5: Int\n\n  \"\"\"\n  regex: \\d+\n  \"\"\"\n  f6: Int\n\n  \"\\nLeading empty line to preserve\"\n  f7: Int\n\n  \" \\nLeading whitespace-only line to preserve\"\n  f8: Int\n\n  \"Trailing empty line to preserve\\n\"\n  f9: Int\n\n  \"Trailing whitespace-only line to preserve\\n\\t\"\n  f10: Int\n\n  f11(arg: String = \"a\\nb\"): Int\n\n  f12(arg: String = \"a \\\"b\\\" c\"): Int\n\n  f13(arg: String = \"regex: \\\\d+\"): Int\n\n  \"Trailing backslash \\\\\"\n  f14: Int\n\n  \"Trailing quote\\\"\"\n  f15: Int\n}\n",
+        },
+    },
+    anonymous_operation: None,
+    named_operations: {},
+    fragments: {},
+}
diff --git a/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.graphql b/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.graphql
new file mode 100644
index 0000000..6b8a4a5
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.graphql
@@ -0,0 +1,37 @@
+# Different fragments selecting the same name from different types
+# into different subselections -- all valid, unless apollo-rs
+# propagates type conditions incorrectly :)
+
+type Subselection {
+  createdAt: Int!
+}
+
+type Issue {
+  createdAt: Int
+  subselection: [Subselection!]
+}
+
+type Query {
+  issue755: Issue
+}
+
+fragment topLevelFragment on Issue {
+  subselection {
+    createdAt
+    ...subselectionFragment
+  }
+  ...collidingTopLevelFragment
+}
+fragment collidingTopLevelFragment on Issue {
+  subselection {
+    createdAt
+  }
+}
+fragment subselectionFragment on Subselection {
+  createdAt
+}
+query  {
+  issue755 {
+    ...topLevelFragment
+  }
+}
diff --git a/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.txt b/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.txt
new file mode 100644
index 0000000..36ba694
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.txt
@@ -0,0 +1,331 @@
+Schema {
+    sources: {
+        -1: SourceFile {
+            path: "built_in.graphql",
+            source_text: include_str!("built_in.graphql"),
+        },
+        39: SourceFile {
+            path: "0040_field_merging_issue_755.graphql",
+            source_text: "# Different fragments selecting the same name from different types\n# into different subselections -- all valid, unless apollo-rs\n# propagates type conditions incorrectly :)\n\ntype Subselection {\n  createdAt: Int!\n}\n\ntype Issue {\n  createdAt: Int\n  subselection: [Subselection!]\n}\n\ntype Query {\n  issue755: Issue\n}\n\nfragment topLevelFragment on Issue {\n  subselection {\n    createdAt\n    ...subselectionFragment\n  }\n  ...collidingTopLevelFragment\n}\nfragment collidingTopLevelFragment on Issue {\n  subselection {\n    createdAt\n  }\n}\nfragment subselectionFragment on Subselection {\n  createdAt\n}\nquery  {\n  issue755 {\n    ...topLevelFragment\n  }\n}\n",
+        },
+    },
+    schema_definition: SchemaDefinition {
+        description: None,
+        directives: [],
+        query: Some(
+            ComponentName {
+                origin: Definition,
+                name: "Query",
+            },
+        ),
+        mutation: None,
+        subscription: None,
+    },
+    directive_definitions: {
+        "skip": built_in_directive!("skip"),
+        "include": built_in_directive!("include"),
+        "deprecated": built_in_directive!("deprecated"),
+        "specifiedBy": built_in_directive!("specifiedBy"),
+    },
+    types: {
+        "__Schema": built_in_type!("__Schema"),
+        "__Type": built_in_type!("__Type"),
+        "__TypeKind": built_in_type!("__TypeKind"),
+        "__Field": built_in_type!("__Field"),
+        "__InputValue": built_in_type!("__InputValue"),
+        "__EnumValue": built_in_type!("__EnumValue"),
+        "__Directive": built_in_type!("__Directive"),
+        "__DirectiveLocation": built_in_type!("__DirectiveLocation"),
+        "Int": built_in_type!("Int"),
+        "Float": built_in_type!("Float"),
+        "String": built_in_type!("String"),
+        "Boolean": built_in_type!("Boolean"),
+        "ID": built_in_type!("ID"),
+        "Subselection": Object(
+            174..213 @39 ObjectType {
+                description: None,
+                name: "Subselection",
+                implements_interfaces: {},
+                directives: [],
+                fields: {
+                    "createdAt": Component {
+                        origin: Definition,
+                        node: 196..211 @39 FieldDefinition {
+                            description: None,
+                            name: "createdAt",
+                            arguments: [],
+                            ty: NonNullNamed(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                },
+            },
+        ),
+        "Issue": Object(
+            215..278 @39 ObjectType {
+                description: None,
+                name: "Issue",
+                implements_interfaces: {},
+                directives: [],
+                fields: {
+                    "createdAt": Component {
+                        origin: Definition,
+                        node: 230..244 @39 FieldDefinition {
+                            description: None,
+                            name: "createdAt",
+                            arguments: [],
+                            ty: Named(
+                                "Int",
+                            ),
+                            directives: [],
+                        },
+                    },
+                    "subselection": Component {
+                        origin: Definition,
+                        node: 247..276 @39 FieldDefinition {
+                            description: None,
+                            name: "subselection",
+                            arguments: [],
+                            ty: List(
+                                NonNullNamed(
+                                    "Subselection",
+                                ),
+                            ),
+                            directives: [],
+                        },
+                    },
+                },
+            },
+        ),
+        "Query": Object(
+            280..312 @39 ObjectType {
+                description: None,
+                name: "Query",
+                implements_interfaces: {},
+                directives: [],
+                fields: {
+                    "issue755": Component {
+                        origin: Definition,
+                        node: 295..310 @39 FieldDefinition {
+                            description: None,
+                            name: "issue755",
+                            arguments: [],
+                            ty: Named(
+                                "Issue",
+                            ),
+                            directives: [],
+                        },
+                    },
+                },
+            },
+        ),
+    },
+}
+ExecutableDocument {
+    sources: {
+        -1: SourceFile {
+            path: "built_in.graphql",
+            source_text: include_str!("built_in.graphql"),
+        },
+        39: SourceFile {
+            path: "0040_field_merging_issue_755.graphql",
+            source_text: "# Different fragments selecting the same name from different types\n# into different subselections -- all valid, unless apollo-rs\n# propagates type conditions incorrectly :)\n\ntype Subselection {\n  createdAt: Int!\n}\n\ntype Issue {\n  createdAt: Int\n  subselection: [Subselection!]\n}\n\ntype Query {\n  issue755: Issue\n}\n\nfragment topLevelFragment on Issue {\n  subselection {\n    createdAt\n    ...subselectionFragment\n  }\n  ...collidingTopLevelFragment\n}\nfragment collidingTopLevelFragment on Issue {\n  subselection {\n    createdAt\n  }\n}\nfragment subselectionFragment on Subselection {\n  createdAt\n}\nquery  {\n  issue755 {\n    ...topLevelFragment\n  }\n}\n",
+        },
+    },
+    anonymous_operation: Some(
+        592..643 @39 Operation {
+            operation_type: Query,
+            name: None,
+            variables: [],
+            directives: [],
+            selection_set: SelectionSet {
+                ty: "Query",
+                selections: [
+                    Field(
+                        603..641 @39 Field {
+                            definition: 295..310 @39 FieldDefinition {
+                                description: None,
+                                name: "issue755",
+                                arguments: [],
+                                ty: Named(
+                                    "Issue",
+                                ),
+                                directives: [],
+                            },
+                            alias: None,
+                            name: "issue755",
+                            arguments: [],
+                            directives: [],
+                            selection_set: SelectionSet {
+                                ty: "Issue",
+                                selections: [
+                                    FragmentSpread(
+                                        618..637 @39 FragmentSpread {
+                                            fragment_name: "topLevelFragment",
+                                            directives: [],
+                                        },
+                                    ),
+                                ],
+                            },
+                        },
+                    ),
+                ],
+            },
+        },
+    ),
+    named_operations: {},
+    fragments: {
+        "topLevelFragment": 314..446 @39 Fragment {
+            name: "topLevelFragment",
+            directives: [],
+            selection_set: SelectionSet {
+                ty: "Issue",
+                selections: [
+                    Field(
+                        353..413 @39 Field {
+                            definition: 247..276 @39 FieldDefinition {
+                                description: None,
+                                name: "subselection",
+                                arguments: [],
+                                ty: List(
+                                    NonNullNamed(
+                                        "Subselection",
+                                    ),
+                                ),
+                                directives: [],
+                            },
+                            alias: None,
+                            name: "subselection",
+                            arguments: [],
+                            directives: [],
+                            selection_set: SelectionSet {
+                                ty: "Subselection",
+                                selections: [
+                                    Field(
+                                        372..381 @39 Field {
+                                            definition: 196..211 @39 FieldDefinition {
+                                                description: None,
+                                                name: "createdAt",
+                                                arguments: [],
+                                                ty: NonNullNamed(
+                                                    "Int",
+                                                ),
+                                                directives: [],
+                                            },
+                                            alias: None,
+                                            name: "createdAt",
+                                            arguments: [],
+                                            directives: [],
+                                            selection_set: SelectionSet {
+                                                ty: "Int",
+                                                selections: [],
+                                            },
+                                        },
+                                    ),
+                                    FragmentSpread(
+                                        386..409 @39 FragmentSpread {
+                                            fragment_name: "subselectionFragment",
+                                            directives: [],
+                                        },
+                                    ),
+                                ],
+                            },
+                        },
+                    ),
+                    FragmentSpread(
+                        416..444 @39 FragmentSpread {
+                            fragment_name: "collidingTopLevelFragment",
+                            directives: [],
+                        },
+                    ),
+                ],
+            },
+        },
+        "collidingTopLevelFragment": 447..529 @39 Fragment {
+            name: "collidingTopLevelFragment",
+            directives: [],
+            selection_set: SelectionSet {
+                ty: "Issue",
+                selections: [
+                    Field(
+                        495..527 @39 Field {
+                            definition: 247..276 @39 FieldDefinition {
+                                description: None,
+                                name: "subselection",
+                                arguments: [],
+                                ty: List(
+                                    NonNullNamed(
+                                        "Subselection",
+                                    ),
+                                ),
+                                directives: [],
+                            },
+                            alias: None,
+                            name: "subselection",
+                            arguments: [],
+                            directives: [],
+                            selection_set: SelectionSet {
+                                ty: "Subselection",
+                                selections: [
+                                    Field(
+                                        514..523 @39 Field {
+                                            definition: 196..211 @39 FieldDefinition {
+                                                description: None,
+                                                name: "createdAt",
+                                                arguments: [],
+                                                ty: NonNullNamed(
+                                                    "Int",
+                                                ),
+                                                directives: [],
+                                            },
+                                            alias: None,
+                                            name: "createdAt",
+                                            arguments: [],
+                                            directives: [],
+                                            selection_set: SelectionSet {
+                                                ty: "Int",
+                                                selections: [],
+                                            },
+                                        },
+                                    ),
+                                ],
+                            },
+                        },
+                    ),
+                ],
+            },
+        },
+        "subselectionFragment": 530..591 @39 Fragment {
+            name: "subselectionFragment",
+            directives: [],
+            selection_set: SelectionSet {
+                ty: "Subselection",
+                selections: [
+                    Field(
+                        580..589 @39 Field {
+                            definition: 196..211 @39 FieldDefinition {
+                                description: None,
+                                name: "createdAt",
+                                arguments: [],
+                                ty: NonNullNamed(
+                                    "Int",
+                                ),
+                                directives: [],
+                            },
+                            alias: None,
+                            name: "createdAt",
+                            arguments: [],
+                            directives: [],
+                            selection_set: SelectionSet {
+                                ty: "Int",
+                                selections: [],
+                            },
+                        },
+                    ),
+                ],
+            },
+        },
+    },
+}
diff --git a/crates/apollo-compiler/test_data/serializer/ok/0039_string_literals.graphql b/crates/apollo-compiler/test_data/serializer/ok/0039_string_literals.graphql
new file mode 100644
index 0000000..fcfa92b
--- /dev/null
+++ b/crates/apollo-compiler/test_data/serializer/ok/0039_string_literals.graphql
@@ -0,0 +1,40 @@
+type Query {
+  """
+  LF: a
+  b
+  """
+  f1: Int
+  "CRLF: a\r\nb"
+  f2: Int
+  """
+  a
+
+  b
+  """
+  f3: Int
+  """a "b" c"""
+  f4: Int
+  """a \"""b\""" c"""
+  f5: Int
+  """regex: \d+"""
+  f6: Int
+  "\nLeading empty line to preserve"
+  f7: Int
+  " \nLeading whitespace-only line to preserve"
+  f8: Int
+  "Trailing empty line to preserve\n"
+  f9: Int
+  "Trailing whitespace-only line to preserve\n	"
+  f10: Int
+  f11(arg: String = "a\nb"): Int
+  f12(arg: String = "a \"b\" c"): Int
+  f13(arg: String = "regex: \\d+"): Int
+  """
+  Trailing backslash \
+  """
+  f14: Int
+  """
+  Trailing quote"
+  """
+  f15: Int
+}
diff --git a/crates/apollo-compiler/test_data/serializer/ok/0040_field_merging_issue_755.graphql b/crates/apollo-compiler/test_data/serializer/ok/0040_field_merging_issue_755.graphql
new file mode 100644
index 0000000..6181612
--- /dev/null
+++ b/crates/apollo-compiler/test_data/serializer/ok/0040_field_merging_issue_755.graphql
@@ -0,0 +1,36 @@
+type Subselection {
+  createdAt: Int!
+}
+
+type Issue {
+  createdAt: Int
+  subselection: [Subselection!]
+}
+
+type Query {
+  issue755: Issue
+}
+
+fragment topLevelFragment on Issue {
+  subselection {
+    createdAt
+    ...subselectionFragment
+  }
+  ...collidingTopLevelFragment
+}
+
+fragment collidingTopLevelFragment on Issue {
+  subselection {
+    createdAt
+  }
+}
+
+fragment subselectionFragment on Subselection {
+  createdAt
+}
+
+query {
+  issue755 {
+    ...topLevelFragment
+  }
+}
diff --git a/crates/apollo-compiler/tests/validation/mod.rs b/crates/apollo-compiler/tests/validation/mod.rs
index 686ad0f..e76be11 100644
--- a/crates/apollo-compiler/tests/validation/mod.rs
+++ b/crates/apollo-compiler/tests/validation/mod.rs
@@ -1,6 +1,7 @@
 mod interface;
 mod object;
 mod operation;
+mod recursion;
 mod types;
 mod variable;
 
diff --git a/crates/apollo-compiler/tests/validation/recursion.rs b/crates/apollo-compiler/tests/validation/recursion.rs
new file mode 100644
index 0000000..6639e01
--- /dev/null
+++ b/crates/apollo-compiler/tests/validation/recursion.rs
@@ -0,0 +1,178 @@
+fn build_fragment_chain(size: usize) -> String {
+    let mut query = r#"
+      query Introspection{
+        __schema {
+           types {
+            ...typeFragment1
+          }
+        }
+      }
+    "#
+    .to_string();
+
+    for i in 1..size {
+        query.push_str(&format!(
+            "
+          fragment typeFragment{i} on __Type {{
+            ofType {{
+              ...typeFragment{}
+            }}
+          }}",
+            i + 1
+        ));
+    }
+    query.push_str(&format!(
+        "
+          fragment typeFragment{size} on __Type {{
+            ofType {{
+              name
+            }}
+          }}"
+    ));
+
+    query
+}
+
+fn build_directive_chain(size: usize) -> String {
+    let mut schema = r#"
+      type Query {
+        field: Int! @directive(arg: true)
+      }
+
+      directive @directive(arg: Boolean @argDir1) on FIELD_DEFINITION
+    "#
+    .to_string();
+
+    for i in 1..size {
+        schema.push_str(&format!(
+            "
+            directive @argDir{i}(arg: Boolean @argDir{}) on ARGUMENT_DEFINITION
+            ",
+            i + 1
+        ));
+    }
+    schema.push_str(&format!(
+        "
+        directive @argDir{size}(arg: Boolean) on ARGUMENT_DEFINITION
+          "
+    ));
+
+    schema
+}
+
+fn build_input_object_chain(size: usize) -> String {
+    let mut schema = r#"
+      type Query {
+        field(arg: VeryVeryDeep): Boolean
+      }
+
+      input VeryVeryDeep {
+        nest: VeryVeryDeep1!
+      }
+    "#
+    .to_string();
+
+    for i in 1..size {
+        schema.push_str(&format!(
+            "
+            input VeryVeryDeep{i} {{ nest: VeryVeryDeep{}! }}
+            ",
+            i + 1
+        ));
+    }
+    schema.push_str(&format!(
+        "
+        input VeryVeryDeep{size} {{ final: Boolean }}
+          "
+    ));
+
+    schema
+}
+
+#[test]
+fn long_fragment_chains_do_not_overflow_stack() {
+    // Build a query that applies 1K fragments
+    // Validating it would take a lot of recursion and blow the stack
+    let query = build_fragment_chain(1_000);
+
+    let errors = apollo_compiler::parse_mixed_validate(
+        format!(
+            "type Query {{ a: Int }}
+            {query}"
+        ),
+        "overflow.graphql",
+    )
+    .expect_err("must have recursion errors");
+
+    let expected = expect_test::expect![[r#"
+        Error: too much recursion
+        Error: `typeFragment1` contains too much nesting
+            ╭─[overflow.graphql:11:11]
+            │
+         11 │           fragment typeFragment1 on __Type {
+            │           ───────────┬──────────  
+            │                      ╰──────────── fragment references a very long chain of fragments
+        ────╯
+    "#]];
+    expected.assert_eq(&errors.to_string_no_color());
+}
+
+#[test]
+fn not_long_enough_fragment_chain_applies_correctly() {
+    // Stay just under the recursion limit
+    let query = build_fragment_chain(99);
+
+    let _ = apollo_compiler::parse_mixed_validate(
+        format!(
+            "type Query {{ a: Int }}
+            {query}"
+        ),
+        "no_overflow.graphql",
+    )
+    .expect("must not have recursion errors");
+}
+
+#[test]
+fn long_directive_chains_do_not_overflow_stack() {
+    // Build a schema that defines hundreds of directives that all use each other in their
+    // argument list
+    // Validating it would take a lot of recursion and a lot of time
+    let schema = build_directive_chain(500);
+
+    let partial = apollo_compiler::Schema::parse_and_validate(schema, "directives.graphql")
+        .expect_err("must have recursion errors");
+
+    assert_eq!(partial.errors.len(), 469);
+}
+
+#[test]
+fn not_long_enough_directive_chain_applies_correctly() {
+    // Stay just under the recursion limit
+    let schema = build_directive_chain(31);
+
+    let _schema = apollo_compiler::Schema::parse_and_validate(schema, "directives.graphql")
+        .expect("must not have recursion errors");
+}
+
+#[test]
+fn long_input_object_chains_do_not_overflow_stack() {
+    // Build a very deeply nested input object
+    // Validating it would take a lot of recursion and a lot of time
+    let schema = build_input_object_chain(500);
+
+    let partial = apollo_compiler::Schema::parse_and_validate(schema, "input_objects.graphql")
+        .expect_err("must have recursion errors");
+
+    // The final 199 input objects do not cause recursion errors because the chain is less than 200
+    // directives deep.
+    assert_eq!(partial.errors.len(), 469);
+}
+
+#[test]
+fn not_long_enough_input_object_chain_applies_correctly() {
+    // Stay just under the recursion limit
+    let schema = build_input_object_chain(31);
+
+    let _schema = apollo_compiler::Schema::parse_and_validate(schema, "input_objects.graphql")
+        .expect("must not have recursion errors");
+}