Repository: apollographql/apollo-rs
Pull request #885 (2024-07-19 15:20:34)

Assorted API tweaks + prepare apollo_compiler@1.0.0 beta.19
Moved from the crate root to the new(ly public) `apollo_compiler::parser` module:
* `Parser`
* `SourceFile`
* `SourceMap`
* `FileId`

Moved to the `parser` module and renamed:
* `NodeLocation` → `SourceSpan`
* `GraphQLLocation` → `LineColumn`

Added a few helper methods:
* `executable::OperationMap::from_one`
* `schema::SchemaDefinition::iter_root_operations`
* `schema::ExtendedType::is_leaf`


SimonSapin: New crate root:
![image](https://github.com/user-attachments/assets/61df6552-d825-4617-855f-1c405bd26307)

`parser` module:
![image](https://github.com/user-attachments/assets/26485471-602d-4aec-a5ea-63a062663a1a)


Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index cc504de..ea05c21 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -17,7 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 ## Maintenance
 ## Documentation-->
 
-# [x.x.x] (unreleased) - 2024-mm-dd
+# [1.0.0-beta.19](https://crates.io/crates/apollo-compiler/1.0.0-beta.19) - 2024-07-19
 
 ## BREAKING
 
@@ -38,15 +38,61 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
   This change makes `get_mut()` borrow only `doc.operations` instead of the entire document,
   making it possible to also mutate `doc.fragments` during that mutable borrow.
 
+- **Move or rename some import paths - [SimonSapin] in [pull/885].**
+  Moved from the crate root to the new(ly public) `apollo_compiler::parser` module:
+  * `Parser`
+  * `SourceFile`
+  * `SourceMap`
+  * `FileId`
+
+  Moved to the `parser` module and renamed:
+  * `NodeLocation` → `SourceSpan`
+  * `GraphQLLocation` → `LineColumn`
+
+- **Return ranges of line/column locations - [dylan-apollo] in [pull/861].**
+  `SourceSpan` contains a file ID and a range of UTF-8 offsets within that file.
+  Various APIs can convert offsets to line and column numbers,
+  but often only considered the start offset.
+  They are now changed to consider the range of start and end positions.
+  Added, returning `Option<Range<LineColumn>>`:
+  * `SourceSpan::line_column_range`
+  * `Diagnostic::line_column_range`
+  * `Name::line_column_range`
+
+  Removed:
+  * `LineColumn::from_node`
+  * `Diagnostic::get_line_column`
+  * `SourceFile::get_line_column`
+
+- **Use a fast hasher - [o0Ignition0o] in [pull/881].**
+  Configured all hash-based collections used in apollo-compiler to use `ahash`,
+  which is faster than the default standard library hasher.
+  `apollo_compiler::collections` provides type aliases for collections
+  configured with the same hasher as collections in various parts of the public API.
+
+## Features
+
+- **Add a few helper methods - [SimonSapin] in [pull/885]:**
+  * `executable::OperationMap::from_one`
+  * `schema::SchemaDefinition::iter_root_operations()`
+  * `schema::ExtendedType::is_leaf`
+
 ## Fixes
 
 - **Fix potential hash collision bug in validation - [goto-bus-stop] in [pull/878]**
+- **Fix validation for undefined variables nested in values - [goto-bus-stop] in [pull/885]**
 
 [SimonSapin]: https://github.com/SimonSapin
 [goto-bus-stop]: https://github.com/goto-bus-stop
+[o0Ignition0o]: https://github.com/o0Ignition0o
+[dylan-apollo]: https://github.com/dylan-apollo
+[pull/861]: https://github.com/apollographql/apollo-rs/pull/861
 [pull/877]: https://github.com/apollographql/apollo-rs/pull/877
 [pull/878]: https://github.com/apollographql/apollo-rs/pull/878
 [pull/879]: https://github.com/apollographql/apollo-rs/pull/879
+[pull/881]: https://github.com/apollographql/apollo-rs/pull/881
+[pull/883]: https://github.com/apollographql/apollo-rs/pull/883
+[pull/885]: https://github.com/apollographql/apollo-rs/pull/885
 
 
 # [1.0.0-beta.18](https://crates.io/crates/apollo-compiler/1.0.0-beta.18) - 2024-06-27
diff --git a/crates/apollo-compiler/Cargo.toml b/crates/apollo-compiler/Cargo.toml
index 6f18efe..44ce84a 100644
--- a/crates/apollo-compiler/Cargo.toml
+++ b/crates/apollo-compiler/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "apollo-compiler"
-version = "1.0.0-beta.18" # When bumping, also update README.md
+version = "1.0.0-beta.19" # When bumping, also update README.md
 authors = ["Irina Shestak <shestak.irina@gmail.com>"]
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/apollographql/apollo-rs"
diff --git a/crates/apollo-compiler/README.md b/crates/apollo-compiler/README.md
index 023a852..6db4318 100644
--- a/crates/apollo-compiler/README.md
+++ b/crates/apollo-compiler/README.md
@@ -40,7 +40,7 @@ Or add this to your `Cargo.toml` for a manual installation:
 # Just an example, change to the necessary package version.
 # Using an exact dependency is recommended for beta versions
 [dependencies]
-apollo-compiler = "=1.0.0-beta.18"
+apollo-compiler = "=1.0.0-beta.19"
 ```
 
 ## Rust versions
diff --git a/crates/apollo-compiler/src/ast/from_cst.rs b/crates/apollo-compiler/src/ast/from_cst.rs
index 9a06b1a..815375f 100644
--- a/crates/apollo-compiler/src/ast/from_cst.rs
+++ b/crates/apollo-compiler/src/ast/from_cst.rs
@@ -1,9 +1,9 @@
 use crate::ast;
 use crate::ast::Document;
-use crate::validation::FileId;
+use crate::parser::FileId;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use crate::Node;
-use crate::NodeLocation;
-use crate::SourceMap;
 use apollo_parser::cst;
 use apollo_parser::cst::CstNode;
 use apollo_parser::SyntaxNode;
@@ -28,7 +28,7 @@ pub(crate) trait Convert {
 }
 
 fn with_location<T>(file_id: FileId, syntax_node: &SyntaxNode, node: T) -> Node<T> {
-    Node::new_parsed(node, NodeLocation::new(file_id, syntax_node))
+    Node::new_parsed(node, SourceSpan::new(file_id, syntax_node))
 }
 
 /// Convert and collect, silently skipping entries with conversion errors
@@ -429,7 +429,7 @@ impl Convert for cst::Description {
     fn convert(&self, file_id: FileId) -> Option<Self::Target> {
         Some(Node::new_str_parsed(
             &String::from(self.string_value()?),
-            NodeLocation::new(file_id, self.syntax()),
+            SourceSpan::new(file_id, self.syntax()),
         ))
     }
 }
@@ -762,7 +762,7 @@ impl Convert for cst::Name {
     type Target = crate::Name;
 
     fn convert(&self, file_id: FileId) -> Option<Self::Target> {
-        let loc = NodeLocation::new(file_id, self.syntax());
+        let loc = SourceSpan::new(file_id, self.syntax());
         let token = &self.syntax().first_token()?;
         let str = token.text();
         debug_assert!(crate::Name::is_valid_syntax(str));
diff --git a/crates/apollo-compiler/src/ast/impls.rs b/crates/apollo-compiler/src/ast/impls.rs
index c6461d4..e8d091a 100644
--- a/crates/apollo-compiler/src/ast/impls.rs
+++ b/crates/apollo-compiler/src/ast/impls.rs
@@ -1,12 +1,12 @@
 use super::*;
 use crate::name;
-use crate::node::NodeLocation;
+use crate::parser::Parser;
+use crate::parser::SourceSpan;
 use crate::schema::SchemaBuilder;
 use crate::validation::DiagnosticList;
 use crate::validation::Valid;
 use crate::validation::WithErrors;
 use crate::ExecutableDocument;
-use crate::Parser;
 use crate::Schema;
 use std::fmt;
 use std::hash;
@@ -212,7 +212,7 @@ impl Definition {
         }
     }
 
-    pub fn location(&self) -> Option<NodeLocation> {
+    pub fn location(&self) -> Option<SourceSpan> {
         match self {
             Self::OperationDefinition(def) => def.location(),
             Self::FragmentDefinition(def) => def.location(),
@@ -853,7 +853,7 @@ impl EnumValueDefinition {
 }
 
 impl Selection {
-    pub fn location(&self) -> Option<NodeLocation> {
+    pub fn location(&self) -> Option<SourceSpan> {
         match self {
             Self::Field(field) => field.location(),
             Self::FragmentSpread(fragment) => fragment.location(),
diff --git a/crates/apollo-compiler/src/ast/mod.rs b/crates/apollo-compiler/src/ast/mod.rs
index 6b45004..70d858a 100644
--- a/crates/apollo-compiler/src/ast/mod.rs
+++ b/crates/apollo-compiler/src/ast/mod.rs
@@ -15,7 +15,8 @@
 //!
 //! ## Parsing
 //!
-//! Start with [`Document::parse`], or [`Parser`][crate::Parser] to change the parser configuration.
+//! Start with [`Document::parse`], or [`Parser`][crate::parser::Parser]
+//! to change the parser configuration.
 //!
 //! ## Structural sharing and mutation
 //!
@@ -31,6 +32,7 @@
 //! that has chaining methods for setting serialization configuration,
 //! and also implements `Display` and `ToString`.
 
+use crate::parser::SourceMap;
 use crate::Name;
 use crate::Node;
 
@@ -46,7 +48,7 @@ pub struct Document {
     /// this map contains one entry for that file and its ID.
     ///
     /// The document may have been modified since.
-    pub sources: crate::SourceMap,
+    pub sources: SourceMap,
 
     pub definitions: Vec<Definition>,
 }
diff --git a/crates/apollo-compiler/src/diagnostic.rs b/crates/apollo-compiler/src/diagnostic.rs
index 417794a..548b72b 100644
--- a/crates/apollo-compiler/src/diagnostic.rs
+++ b/crates/apollo-compiler/src/diagnostic.rs
@@ -4,7 +4,7 @@
 //! To use pretty-printing in custom errors, implement the [`ToCliReport`] trait.
 //!
 //! ```rust
-//! use apollo_compiler::NodeLocation;
+//! use apollo_compiler::parser::SourceSpan;
 //! use apollo_compiler::Schema;
 //! use apollo_compiler::Name;
 //! use apollo_compiler::diagnostic::CliReport;
@@ -18,13 +18,13 @@
 //!     InvalidCase { name: Name },
 //!     #[error("Missing @specifiedBy directive on scalar {name}")]
 //!     NoSpecifiedBy {
-//!         location: Option<NodeLocation>,
+//!         location: Option<SourceSpan>,
 //!         name: Name,
 //!     },
 //! }
 //!
 //! impl ToCliReport for LintError {
-//!     fn location(&self) -> Option<NodeLocation> {
+//!     fn location(&self) -> Option<SourceSpan> {
 //!         match self {
 //!             LintError::InvalidCase { name } => name.location(),
 //!             LintError::NoSpecifiedBy { location, .. } => *location,
@@ -52,12 +52,12 @@
 //! ready for formatting:
 //!
 //! ```rust
-//! # use apollo_compiler::{NodeLocation, Schema, diagnostic::{ToCliReport, CliReport}};
+//! # use apollo_compiler::{parser::SourceSpan, Schema, diagnostic::{ToCliReport, CliReport}};
 //! # #[derive(Debug, thiserror::Error)]
 //! # #[error("")]
 //! # struct LintError {}
 //! # impl ToCliReport for LintError {
-//! #     fn location(&self) -> Option<NodeLocation> { None }
+//! #     fn location(&self) -> Option<SourceSpan> { None }
 //! #     fn report(&self, _report: &mut CliReport) {}
 //! # }
 //! fn print_errors(schema: &Schema, errors: &[LintError]) {
@@ -68,15 +68,15 @@
 //! }
 //! ```
 use crate::execution::GraphQLError;
-use crate::execution::GraphQLLocation;
-use crate::validation::FileId;
+use crate::parser::FileId;
+use crate::parser::LineColumn;
+use crate::parser::SourceFile;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 #[cfg(doc)]
 use crate::ExecutableDocument;
-use crate::NodeLocation;
 #[cfg(doc)]
 use crate::Schema;
-use crate::SourceFile;
-use crate::SourceMap;
 use ariadne::ColorGenerator;
 use ariadne::ReportKind;
 use std::cell::Cell;
@@ -128,7 +128,7 @@ pub enum Color {
 pub trait ToCliReport: fmt::Display {
     /// Return the main location for this error. May be `None` if a location doesn't make sense for
     /// the particular error.
-    fn location(&self) -> Option<NodeLocation>;
+    fn location(&self) -> Option<SourceSpan>;
 
     /// Fill in the report with source code labels.
     ///
@@ -157,7 +157,7 @@ pub trait ToCliReport: fmt::Display {
 }
 
 impl<T: ToCliReport> ToCliReport for &T {
-    fn location(&self) -> Option<NodeLocation> {
+    fn location(&self) -> Option<SourceSpan> {
         ToCliReport::location(*self)
     }
 
@@ -166,12 +166,12 @@ impl<T: ToCliReport> ToCliReport for &T {
     }
 }
 
-/// An ariadne span type. We avoid implementing `ariadne::Span` for `NodeLocation`
+/// An ariadne span type. We avoid implementing `ariadne::Span` for `SourceSpan`
 /// so ariadne doesn't leak into the public API of apollo-compiler.
 type AriadneSpan = (FileId, Range<usize>);
 
-/// Translate a NodeLocation into an ariadne span type.
-fn to_span(location: NodeLocation) -> Option<AriadneSpan> {
+/// Translate a SourceSpan into an ariadne span type.
+fn to_span(location: SourceSpan) -> Option<AriadneSpan> {
     let start = location.offset();
     let end = location.end_offset();
     Some((location.file_id, start..end))
@@ -201,7 +201,7 @@ impl<'s> CliReport<'s> {
     /// Source files can be obtained from [`Schema::sources`] or [`ExecutableDocument::sources`].
     pub fn builder(
         sources: &'s SourceMap,
-        main_location: Option<NodeLocation>,
+        main_location: Option<SourceSpan>,
         color: Color,
     ) -> Self {
         let (file_id, range) = main_location
@@ -241,7 +241,7 @@ impl<'s> CliReport<'s> {
     }
 
     /// Add a label at a given location. If the location is `None`, the message is discarded.
-    pub fn with_label_opt(&mut self, location: Option<NodeLocation>, message: impl ToString) {
+    pub fn with_label_opt(&mut self, location: Option<SourceSpan>, message: impl ToString) {
         if let Some(span) = location.and_then(to_span) {
             self.report.add_label(
                 ariadne::Label::new(span)
@@ -326,7 +326,7 @@ impl<T: ToCliReport> std::error::Error for Diagnostic<'_, T> {}
 
 impl<T: ToCliReport> Diagnostic<'_, T> {
     /// Get the line and column numbers where this diagnostic spans.
-    pub fn line_column_range(&self) -> Option<Range<GraphQLLocation>> {
+    pub fn line_column_range(&self) -> Option<Range<LineColumn>> {
         self.error.location()?.line_column_range(self.sources)
     }
 
diff --git a/crates/apollo-compiler/src/executable/mod.rs b/crates/apollo-compiler/src/executable/mod.rs
index 34b8d5d..ef7ab5c 100644
--- a/crates/apollo-compiler/src/executable/mod.rs
+++ b/crates/apollo-compiler/src/executable/mod.rs
@@ -2,16 +2,23 @@
 //! which can contain operations and fragments.
 
 use crate::ast;
+use crate::collections::HashSet;
 use crate::collections::IndexMap;
 use crate::coordinate::FieldArgumentCoordinate;
 use crate::coordinate::TypeAttributeCoordinate;
+use crate::parser::Parser;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use crate::schema;
+use crate::validation::DiagnosticList;
+use crate::validation::Valid;
+use crate::validation::WithErrors;
 use crate::Node;
-use crate::Parser;
 use crate::Schema;
 use indexmap::map::Entry;
-use std::collections::HashSet;
+use std::fmt;
 use std::path::Path;
+use std::sync::Arc;
 
 pub(crate) mod from_ast;
 mod serialize;
@@ -25,13 +32,7 @@ pub use crate::ast::OperationType;
 pub use crate::ast::Type;
 pub use crate::ast::Value;
 pub use crate::ast::VariableDefinition;
-use crate::validation::DiagnosticList;
-use crate::validation::Valid;
-use crate::validation::WithErrors;
 pub use crate::Name;
-use crate::NodeLocation;
-use std::fmt;
-use std::sync::Arc;
 
 /// Executable definitions, annotated with type information
 #[derive(Debug, Clone, Default)]
@@ -40,7 +41,7 @@ pub struct ExecutableDocument {
     /// this map contains one entry for that file and its ID.
     ///
     /// The document may have been modified since.
-    pub sources: crate::SourceMap,
+    pub sources: SourceMap,
 
     pub operations: OperationMap,
     pub fragments: FragmentMap,
@@ -64,7 +65,7 @@ pub struct FieldSet {
     /// this map contains one entry for that file and its ID.
     ///
     /// The document may have been modified since.
-    pub sources: crate::SourceMap,
+    pub sources: SourceMap,
 
     pub selection_set: SelectionSet,
 }
@@ -216,10 +217,10 @@ pub(crate) enum BuildError {
 pub(crate) struct ConflictingFieldType {
     /// Name or alias of the non-unique field.
     pub(crate) alias: Name,
-    pub(crate) original_location: Option<NodeLocation>,
+    pub(crate) original_location: Option<SourceSpan>,
     pub(crate) original_coordinate: TypeAttributeCoordinate,
     pub(crate) original_type: Type,
-    pub(crate) conflicting_location: Option<NodeLocation>,
+    pub(crate) conflicting_location: Option<SourceSpan>,
     pub(crate) conflicting_coordinate: TypeAttributeCoordinate,
     pub(crate) conflicting_type: Type,
 }
@@ -229,10 +230,10 @@ pub(crate) struct ConflictingFieldType {
 pub(crate) struct ConflictingFieldArgument {
     /// Name or alias of the non-unique field.
     pub(crate) alias: Name,
-    pub(crate) original_location: Option<NodeLocation>,
+    pub(crate) original_location: Option<SourceSpan>,
     pub(crate) original_coordinate: FieldArgumentCoordinate,
     pub(crate) original_value: Option<Value>,
-    pub(crate) conflicting_location: Option<NodeLocation>,
+    pub(crate) conflicting_location: Option<SourceSpan>,
     pub(crate) conflicting_coordinate: FieldArgumentCoordinate,
     pub(crate) conflicting_value: Option<Value>,
 }
@@ -242,9 +243,9 @@ pub(crate) struct ConflictingFieldArgument {
 pub(crate) struct ConflictingFieldName {
     /// Name of the non-unique field.
     pub(crate) alias: Name,
-    pub(crate) original_location: Option<NodeLocation>,
+    pub(crate) original_location: Option<SourceSpan>,
     pub(crate) original_selection: TypeAttributeCoordinate,
-    pub(crate) conflicting_location: Option<NodeLocation>,
+    pub(crate) conflicting_location: Option<SourceSpan>,
     pub(crate) conflicting_selection: TypeAttributeCoordinate,
 }
 
@@ -270,7 +271,7 @@ pub(crate) enum ExecutableDefinitionName {
     Fragment(Name),
 }
 
-/// A request error returned by [`ExecutableDocument::get_operation`]
+/// A request error returned by [`OperationMap::get`]
 ///
 /// If `get_operation`’s `name_request` argument was `Some`, this error indicates
 /// that the document does not contain an operation with the requested name.
@@ -342,6 +343,13 @@ impl PartialEq for ExecutableDocument {
 }
 
 impl OperationMap {
+    /// Creates a new `OperationMap` containing one operation
+    pub fn from_one(operation: impl Into<Node<Operation>>) -> Self {
+        let mut map = Self::default();
+        map.insert(operation);
+        map
+    }
+
     /// Returns an iterator of operations, both anonymous and named
     pub fn iter(&self) -> impl Iterator<Item = &'_ Node<Operation>> {
         self.anonymous
@@ -473,7 +481,7 @@ impl Operation {
         }
 
         self.operation_type == OperationType::Query
-            && is_introspection_impl(document, &mut HashSet::new(), &self.selection_set)
+            && is_introspection_impl(document, &mut HashSet::default(), &self.selection_set)
     }
 
     serialize_method!();
diff --git a/crates/apollo-compiler/src/executable/serialize.rs b/crates/apollo-compiler/src/executable/serialize.rs
index 51f0a15..84f9ad1 100644
--- a/crates/apollo-compiler/src/executable/serialize.rs
+++ b/crates/apollo-compiler/src/executable/serialize.rs
@@ -26,7 +26,7 @@ impl ExecutableDocument {
 }
 
 impl Operation {
-    fn to_ast(&self, location: Option<NodeLocation>) -> ast::Definition {
+    fn to_ast(&self, location: Option<SourceSpan>) -> ast::Definition {
         let def = ast::OperationDefinition {
             operation_type: self.operation_type,
             name: self.name.clone(),
@@ -43,7 +43,7 @@ impl Operation {
 }
 
 impl Fragment {
-    fn to_ast(&self, location: Option<NodeLocation>) -> ast::Definition {
+    fn to_ast(&self, location: Option<SourceSpan>) -> ast::Definition {
         let def = ast::FragmentDefinition {
             name: self.name.clone(),
             type_condition: self.selection_set.ty.clone(),
diff --git a/crates/apollo-compiler/src/execution/engine.rs b/crates/apollo-compiler/src/execution/engine.rs
index a434e23..3c97e40 100644
--- a/crates/apollo-compiler/src/execution/engine.rs
+++ b/crates/apollo-compiler/src/execution/engine.rs
@@ -1,4 +1,5 @@
 use crate::ast::Value;
+use crate::collections::HashSet;
 use crate::collections::IndexMap;
 use crate::executable::Field;
 use crate::executable::Selection;
@@ -10,7 +11,8 @@ use crate::execution::GraphQLError;
 use crate::execution::JsonMap;
 use crate::execution::JsonValue;
 use crate::execution::ResponseDataPathElement;
-use crate::node::NodeLocation;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use crate::schema::ExtendedType;
 use crate::schema::FieldDefinition;
 use crate::schema::ObjectType;
@@ -20,8 +22,6 @@ use crate::validation::Valid;
 use crate::ExecutableDocument;
 use crate::Name;
 use crate::Schema;
-use crate::SourceMap;
-use std::collections::HashSet;
 
 /// <https://spec.graphql.org/October2021/#sec-Normal-and-Serial-Execution>
 #[derive(Debug, Copy, Clone)]
@@ -66,7 +66,7 @@ pub(crate) fn execute_selection_set<'a>(
         variable_values,
         object_type,
         selections,
-        &mut HashSet::new(),
+        &mut HashSet::default(),
         &mut grouped_field_set,
     );
 
@@ -294,7 +294,7 @@ impl GraphQLError {
     pub(crate) fn field_error(
         message: impl Into<String>,
         path: LinkedPath<'_>,
-        location: Option<NodeLocation>,
+        location: Option<SourceSpan>,
         sources: &SourceMap,
     ) -> Self {
         let mut err = Self::new(message, location, sources);
diff --git a/crates/apollo-compiler/src/execution/input_coercion.rs b/crates/apollo-compiler/src/execution/input_coercion.rs
index c6d63bb..4a967e7 100644
--- a/crates/apollo-compiler/src/execution/input_coercion.rs
+++ b/crates/apollo-compiler/src/execution/input_coercion.rs
@@ -1,5 +1,6 @@
 use crate::ast::Type;
 use crate::ast::Value;
+use crate::collections::HashMap;
 use crate::executable::Field;
 use crate::executable::Operation;
 use crate::execution::engine::LinkedPath;
@@ -8,7 +9,8 @@ use crate::execution::GraphQLError;
 use crate::execution::JsonMap;
 use crate::execution::JsonValue;
 use crate::execution::Response;
-use crate::node::NodeLocation;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use crate::schema::ExtendedType;
 use crate::schema::FieldDefinition;
 use crate::validation::SuspectedValidationBug;
@@ -16,8 +18,6 @@ use crate::validation::Valid;
 use crate::ExecutableDocument;
 use crate::Node;
 use crate::Schema;
-use crate::SourceMap;
-use std::collections::HashMap;
 
 #[derive(Debug, Clone)]
 pub enum InputCoercionError {
@@ -25,7 +25,7 @@ pub enum InputCoercionError {
     // TODO: split into more structured variants?
     ValueError {
         message: String,
-        location: Option<NodeLocation>,
+        location: Option<SourceSpan>,
     },
 }
 
diff --git a/crates/apollo-compiler/src/execution/introspection_execute.rs b/crates/apollo-compiler/src/execution/introspection_execute.rs
index ca44c6a..961e95a 100644
--- a/crates/apollo-compiler/src/execution/introspection_execute.rs
+++ b/crates/apollo-compiler/src/execution/introspection_execute.rs
@@ -1,3 +1,4 @@
+use crate::collections::HashMap;
 use crate::executable::Operation;
 use crate::executable::OperationType;
 use crate::execution::engine::execute_selection_set;
@@ -15,7 +16,6 @@ use crate::ExecutableDocument;
 use crate::Node;
 use crate::Schema;
 use std::borrow::Cow;
-use std::collections::HashMap;
 use std::sync::OnceLock;
 
 /// A document with a single query that only has [schema introspection] fields.
diff --git a/crates/apollo-compiler/src/execution/introspection_split.rs b/crates/apollo-compiler/src/execution/introspection_split.rs
index a36fec5..86e9cbf 100644
--- a/crates/apollo-compiler/src/execution/introspection_split.rs
+++ b/crates/apollo-compiler/src/execution/introspection_split.rs
@@ -12,7 +12,8 @@ use crate::executable::SelectionSet;
 use crate::execution::GraphQLError;
 use crate::execution::Response;
 use crate::execution::SchemaIntrospectionQuery;
-use crate::node::NodeLocation;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use crate::schema;
 use crate::schema::Name;
 use crate::validation::SuspectedValidationBug;
@@ -20,7 +21,6 @@ use crate::validation::Valid;
 use crate::ExecutableDocument;
 use crate::Node;
 use crate::Schema;
-use crate::SourceMap;
 use indexmap::map::Entry;
 
 /// Result of [`split`][Self::split]ting [schema introspection] fields from an operation.
@@ -55,7 +55,8 @@ pub enum SchemaIntrospectionSplit {
         /// and the fragment definitions that are still needed.
         /// The operation definition name is preserved,
         /// so either `None` or the original `Option<&str>` name request can be passed
-        /// to [`ExecutableDocument::get_operation`] to obtain the one operation.
+        /// to [`OperationMap::get`][crate::executable::OperationMap::get]
+        /// to obtain the one operation.
         filtered_document: Valid<ExecutableDocument>,
     },
 }
@@ -64,7 +65,7 @@ pub enum SchemaIntrospectionError {
     SuspectedValidationBug(SuspectedValidationBug),
     Unsupported {
         message: String,
-        location: Option<NodeLocation>,
+        location: Option<SourceSpan>,
     },
 }
 
diff --git a/crates/apollo-compiler/src/execution/mod.rs b/crates/apollo-compiler/src/execution/mod.rs
index ee1d0b6..088e2d7 100644
--- a/crates/apollo-compiler/src/execution/mod.rs
+++ b/crates/apollo-compiler/src/execution/mod.rs
@@ -19,7 +19,6 @@ pub use self::introspection_execute::SchemaIntrospectionQuery;
 pub use self::introspection_split::SchemaIntrospectionError;
 pub use self::introspection_split::SchemaIntrospectionSplit;
 pub use self::response::GraphQLError;
-pub use self::response::GraphQLLocation;
 pub use self::response::Response;
 pub use self::response::ResponseData;
 pub use self::response::ResponseDataPathElement;
diff --git a/crates/apollo-compiler/src/execution/response.rs b/crates/apollo-compiler/src/execution/response.rs
index 5f861bc..091d554 100644
--- a/crates/apollo-compiler/src/execution/response.rs
+++ b/crates/apollo-compiler/src/execution/response.rs
@@ -1,7 +1,8 @@
 use crate::execution::engine::PropagateNull;
 use crate::execution::JsonMap;
-use crate::node::NodeLocation;
-use crate::SourceMap;
+use crate::parser::LineColumn;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use serde::Deserialize;
 use serde::Serialize;
 
@@ -58,7 +59,7 @@ pub struct GraphQLError {
     /// Locations in relevant to the error, if any.
     #[serde(skip_serializing_if = "Vec::is_empty")]
     #[serde(default)]
-    pub locations: Vec<GraphQLLocation>,
+    pub locations: Vec<LineColumn>,
 
     /// If non-empty, the error is a [field error]
     /// for the particular field found at this path in [`Response::data`].
@@ -74,17 +75,6 @@ pub struct GraphQLError {
     pub extensions: JsonMap,
 }
 
-/// A source location (1-indexed line and column numbers) within a GraphQL document.
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct GraphQLLocation {
-    /// The line number for this location, starting at 1 for the first line.
-    pub line: usize,
-    /// The column number for this location, starting at 1 and counting characters (Unicode Scalar
-    /// Values) like [`str::chars`].
-    pub column: usize,
-}
-
 /// An element of [`GraphQLError::path`]
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[serde(untagged)]
@@ -136,7 +126,7 @@ impl Response {
 impl GraphQLError {
     pub fn new(
         message: impl Into<String>,
-        location: Option<NodeLocation>,
+        location: Option<SourceSpan>,
         sources: &SourceMap,
     ) -> Self {
         Self {
diff --git a/crates/apollo-compiler/src/introspection/resolver.rs b/crates/apollo-compiler/src/introspection/resolver.rs
index f00aa20..06e5d0a 100644
--- a/crates/apollo-compiler/src/introspection/resolver.rs
+++ b/crates/apollo-compiler/src/introspection/resolver.rs
@@ -4,8 +4,8 @@ use crate::schema::Name;
 use crate::Node;
 use crate::Schema;
 use std::borrow::Cow;
-use std::collections::HashMap;
-use std::collections::HashSet;
+use crate::collections::HashMap;
+use crate::collections::HashSet;
 use std::sync::OnceLock;
 
 #[derive(Clone, Copy)]
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index 707bd3d..a0ae2d2 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -10,18 +10,13 @@ pub mod executable;
 pub mod execution;
 mod name;
 mod node;
-mod parser;
+pub mod parser;
 pub mod schema;
 pub mod validation;
 
 pub use self::executable::ExecutableDocument;
 pub use self::name::InvalidNameError;
 pub use self::name::Name;
-pub use self::node::FileId;
 pub use self::node::Node;
-pub use self::node::NodeLocation;
 pub use self::parser::parse_mixed_validate;
-pub use self::parser::Parser;
-pub use self::parser::SourceFile;
-pub use self::parser::SourceMap;
 pub use self::schema::Schema;
diff --git a/crates/apollo-compiler/src/name.rs b/crates/apollo-compiler/src/name.rs
index a277eca..1207c96 100644
--- a/crates/apollo-compiler/src/name.rs
+++ b/crates/apollo-compiler/src/name.rs
@@ -1,12 +1,12 @@
 use crate::diagnostic::CliReport;
 use crate::diagnostic::ToCliReport;
-use crate::execution::GraphQLLocation;
-use crate::node::TaggedFileId;
+use crate::parser::FileId;
+use crate::parser::LineColumn;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
+use crate::parser::TaggedFileId;
 use crate::schema::ComponentName;
 use crate::schema::ComponentOrigin;
-use crate::FileId;
-use crate::NodeLocation;
-use crate::SourceMap;
 use rowan::TextRange;
 use std::fmt;
 use std::marker::PhantomData;
@@ -55,7 +55,7 @@ macro_rules! name {
 /// Internally, the string value is either an atomically-reference counted `Arc<str>`
 /// or a `&'static str` borrow that lives until the end of the program.
 //
-// Fields: equivalent to `(UnpackedRepr, Option<NodeLocation>)` but more compact
+// Fields: equivalent to `(UnpackedRepr, Option<SourceSpan>)` but more compact
 pub struct Name {
     /// Data pointer of either `Arc<str>::into_raw` (if `tagged_file_id.tag() == TAG_ARC`)
     /// or `&'static str` (if `TAG_STATIC`)
@@ -78,7 +78,7 @@ enum UnpackedRepr {
 #[error("`{name}` is not a valid GraphQL name")]
 pub struct InvalidNameError {
     pub name: String,
-    pub location: Option<NodeLocation>,
+    pub location: Option<SourceSpan>,
 }
 
 const TAG_ARC: bool = true;
@@ -156,7 +156,7 @@ impl Name {
     }
 
     /// Modifies the given name to add its location in a parsed source file
-    pub fn with_location(mut self, location: NodeLocation) -> Self {
+    pub fn with_location(mut self, location: SourceSpan) -> Self {
         debug_assert_eq!(location.text_range.len(), self.len.into());
         self.start_offset = location.text_range.start().into();
         self.tagged_file_id = TaggedFileId::pack(self.tagged_file_id.tag(), location.file_id);
@@ -173,10 +173,10 @@ impl Name {
 
     /// If this node was parsed from a source file, returns the file ID and source span
     /// (start and end byte offsets) within that file.
-    pub fn location(&self) -> Option<NodeLocation> {
+    pub fn location(&self) -> Option<SourceSpan> {
         let file_id = self.tagged_file_id.file_id();
         if file_id != FileId::NONE {
-            Some(NodeLocation {
+            Some(SourceSpan {
                 file_id,
                 text_range: TextRange::at(self.start_offset.into(), self.len.into()),
             })
@@ -186,7 +186,7 @@ impl Name {
     }
 
     /// If this string contains a location, convert it to line and column numbers
-    pub fn line_column_range(&self, sources: &SourceMap) -> Option<Range<GraphQLLocation>> {
+    pub fn line_column_range(&self, sources: &SourceMap) -> Option<Range<LineColumn>> {
         self.location()?.line_column_range(sources)
     }
 
@@ -502,7 +502,7 @@ impl AsRef<Name> for Name {
 }
 
 impl ToCliReport for InvalidNameError {
-    fn location(&self) -> Option<NodeLocation> {
+    fn location(&self) -> Option<SourceSpan> {
         self.location
     }
     fn report(&self, report: &mut CliReport) {
diff --git a/crates/apollo-compiler/src/node.rs b/crates/apollo-compiler/src/node.rs
index 8208478..15a566f 100644
--- a/crates/apollo-compiler/src/node.rs
+++ b/crates/apollo-compiler/src/node.rs
@@ -1,23 +1,20 @@
-use crate::execution::GraphQLLocation;
+use crate::parser::FileId;
+use crate::parser::LineColumn;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use crate::schema::Component;
 use crate::schema::ComponentOrigin;
-use crate::SourceMap;
-use apollo_parser::SyntaxNode;
-use rowan::TextRange;
 use std::fmt;
 use std::hash::Hash;
 use std::hash::Hasher;
-use std::num::NonZeroU64;
 use std::ops::Range;
-use std::sync::atomic;
-use std::sync::atomic::AtomicU64;
 use triomphe::HeaderSlice;
 
 /// A thread-safe reference-counted smart pointer for GraphQL nodes.
 ///
 /// Similar to [`std::sync::Arc<T>`] but:
 ///
-/// * In addition to `T`, contains an optional [`NodeLocation`].
+/// * In addition to `T`, contains an optional [`SourceSpan`].
 ///   This location notably allows diagnostics to point to relevant parts of parsed input files.
 /// * Weak references are not supported.
 #[derive(serde::Deserialize)]
@@ -26,35 +23,13 @@ pub struct Node<T: ?Sized>(triomphe::Arc<HeaderSlice<Header, T>>);
 
 #[derive(Clone)]
 struct Header {
-    location: Option<NodeLocation>,
-}
-
-/// The source location of a parsed node:
-/// file ID and source span (start and end byte offsets) within that file.
-#[derive(Clone, Copy, Hash, PartialEq, Eq)]
-pub struct NodeLocation {
-    pub(crate) file_id: FileId,
-    pub(crate) text_range: TextRange,
-}
-
-/// Integer identifier for a parsed source file.
-///
-/// Used internally to support validating for example a schema built from multiple source files,
-/// and having diagnostics point to relevant sources.
-#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
-pub struct FileId {
-    id: NonZeroU64,
-}
-
-#[derive(Copy, Clone)]
-pub(crate) struct TaggedFileId {
-    tag_and_id: NonZeroU64,
+    location: Option<SourceSpan>,
 }
 
 impl<T> Node<T> {
     /// Create a new `Node` for something parsed from the given source location
     #[inline]
-    pub fn new_parsed(node: T, location: NodeLocation) -> Self {
+    pub fn new_parsed(node: T, location: SourceSpan) -> Self {
         Self::new_opt_location(node, Some(location))
     }
 
@@ -63,7 +38,7 @@ impl<T> Node<T> {
         Self::new_opt_location(node, None)
     }
 
-    pub(crate) fn new_opt_location(node: T, location: Option<NodeLocation>) -> Self {
+    pub(crate) fn new_opt_location(node: T, location: Option<SourceSpan>) -> Self {
         Self(triomphe::Arc::new(HeaderSlice {
             header: Header { location },
             slice: node,
@@ -74,7 +49,7 @@ impl<T> Node<T> {
 impl Node<str> {
     /// Create a new `Node<str>` for a string parsed from the given source location
     #[inline]
-    pub fn new_str_parsed(node: &str, location: NodeLocation) -> Self {
+    pub fn new_str_parsed(node: &str, location: SourceSpan) -> Self {
         Self::new_str_opt_location(node, Some(location))
     }
 
@@ -83,7 +58,7 @@ impl Node<str> {
         Self::new_str_opt_location(node, None)
     }
 
-    pub(crate) fn new_str_opt_location(node: &str, location: Option<NodeLocation>) -> Self {
+    pub(crate) fn new_str_opt_location(node: &str, location: Option<SourceSpan>) -> Self {
         Self(triomphe::Arc::from_header_and_str(
             Header { location },
             node,
@@ -98,7 +73,7 @@ impl Node<str> {
 impl<T: ?Sized> Node<T> {
     /// If this node was parsed from a source file, returns the file ID and source span
     /// (start and end byte offsets) within that file.
-    pub fn location(&self) -> Option<NodeLocation> {
+    pub fn location(&self) -> Option<SourceSpan> {
         self.0.header.location
     }
 
@@ -109,7 +84,7 @@ impl<T: ?Sized> Node<T> {
     }
 
     /// If this node contains a location, convert it to the line and column numbers.
-    pub fn line_column_range(&self, sources: &SourceMap) -> Option<Range<GraphQLLocation>> {
+    pub fn line_column_range(&self, sources: &SourceMap) -> Option<Range<LineColumn>> {
         self.location()?.line_column_range(sources)
     }
 
@@ -259,82 +234,6 @@ impl From<Node<str>> for String {
     }
 }
 
-impl NodeLocation {
-    pub(crate) fn new(file_id: FileId, node: &'_ SyntaxNode) -> Self {
-        Self {
-            file_id,
-            text_range: node.text_range(),
-        }
-    }
-
-    /// Returns the file ID for this location
-    pub fn file_id(&self) -> FileId {
-        self.file_id
-    }
-
-    /// Returns the offset from the start of the file to the start of the range, in UTF-8 bytes
-    pub fn offset(&self) -> usize {
-        self.text_range.start().into()
-    }
-
-    /// Returns the offset from the start of the file to the end of the range, in UTF-8 bytes
-    ///
-    /// The range is exclusive, so this offset is one past the end of the range.
-    pub fn end_offset(&self) -> usize {
-        self.text_range.end().into()
-    }
-
-    /// Returns the length of the range, in UTF-8 bytes
-    pub fn node_len(&self) -> usize {
-        self.text_range.len().into()
-    }
-
-    /// Best effort at making a location with the given start and end
-    pub fn recompose(start_of: Option<Self>, end_of: Option<Self>) -> Option<Self> {
-        match (start_of, end_of) {
-            (None, None) => None,
-            (None, single @ Some(_)) | (single @ Some(_), None) => single,
-            (Some(start), Some(end)) => {
-                if start.file_id != end.file_id {
-                    // Pick one aribtrarily
-                    return Some(end);
-                }
-                Some(NodeLocation {
-                    file_id: start.file_id,
-                    text_range: TextRange::new(start.text_range.start(), end.text_range.end()),
-                })
-            }
-        }
-    }
-
-    /// The line and column numbers of [`Self::offset`]
-    pub fn line_column(&self, sources: &SourceMap) -> Option<GraphQLLocation> {
-        let source = sources.get(&self.file_id)?;
-        source.get_line_column(self.offset())
-    }
-
-    /// The line and column numbers of the range from [`Self::offset`] to [`Self::end_offset`]
-    /// inclusive.
-    pub fn line_column_range(&self, sources: &SourceMap) -> Option<Range<GraphQLLocation>> {
-        let source = sources.get(&self.file_id)?;
-        let start = source.get_line_column(self.offset())?;
-        let end = source.get_line_column(self.end_offset())?;
-        Some(Range { start, end })
-    }
-}
-
-impl fmt::Debug for NodeLocation {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "{}..{} @{:?}",
-            self.offset(),
-            self.end_offset(),
-            self.file_id,
-        )
-    }
-}
-
 impl<T: serde::Serialize> serde::Serialize for Node<T> {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
@@ -343,95 +242,3 @@ impl<T: serde::Serialize> serde::Serialize for Node<T> {
         T::serialize(self, serializer)
     }
 }
-
-impl fmt::Debug for FileId {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.id.fmt(f)
-    }
-}
-
-/// The next file ID to use. This is global so file IDs do not conflict between different compiler
-/// instances.
-static NEXT: AtomicU64 = AtomicU64::new(INITIAL);
-static INITIAL: u64 = 3;
-
-const TAG: u64 = 1 << 63;
-const ID_MASK: u64 = !TAG;
-
-#[allow(clippy::assertions_on_constants)]
-const _: () = {
-    assert!(TAG == 0x8000_0000_0000_0000);
-    assert!(ID_MASK == 0x7FFF_FFFF_FFFF_FFFF);
-};
-
-impl FileId {
-    /// The ID of the file implicitly added to type systems, for built-in scalars and introspection types
-    pub const BUILT_IN: Self = Self::const_new(1);
-
-    /// Passed to Ariadne to create a report without a location
-    pub(crate) const NONE: Self = Self::const_new(2);
-
-    // Returning a different value every time does not sound like good `impl Default`
-    #[allow(clippy::new_without_default)]
-    pub fn new() -> Self {
-        loop {
-            let id = NEXT.fetch_add(1, atomic::Ordering::AcqRel);
-            if id & TAG == 0 {
-                return Self {
-                    id: NonZeroU64::new(id).unwrap(),
-                };
-            } else {
-                // Overflowing 63 bits is unlikely, but if it somehow happens
-                // reset the counter and try again.
-                //
-                // `TaggedFileId` behaving incorrectly would be a memory safety issue,
-                // whereas a file ID collision “merely” causes
-                // diagnostics to print the wrong file name and source context.
-                Self::reset()
-            }
-        }
-    }
-
-    /// Reset file ID counter back to its initial value, used to get consistent results in tests.
-    ///
-    /// All tests in the process must use `#[serial_test::serial]`
-    #[doc(hidden)]
-    pub fn reset() {
-        NEXT.store(INITIAL, atomic::Ordering::Release)
-    }
-
-    const fn const_new(id: u64) -> Self {
-        assert!(id & ID_MASK == id);
-        // TODO: use unwrap() when const-stable https://github.com/rust-lang/rust/issues/67441
-        if let Some(id) = NonZeroU64::new(id) {
-            Self { id }
-        } else {
-            panic!()
-        }
-    }
-}
-
-impl TaggedFileId {
-    pub(crate) const fn pack(tag: bool, id: FileId) -> Self {
-        debug_assert!((id.id.get() & TAG) == 0);
-        let tag_and_id = if tag {
-            let packed = id.id.get() | TAG;
-            // SAFETY: `id.id` was non-zero, so setting an additional bit is still non-zero
-            unsafe { NonZeroU64::new_unchecked(packed) }
-        } else {
-            id.id
-        };
-        Self { tag_and_id }
-    }
-
-    pub(crate) fn tag(self) -> bool {
-        (self.tag_and_id.get() & TAG) != 0
-    }
-
-    pub(crate) fn file_id(self) -> FileId {
-        let unpacked = self.tag_and_id.get() & ID_MASK;
-        // SAFETY: `unpacked` has the same value as `id: FileId` did in `pack()`, which is non-zero
-        let id = unsafe { NonZeroU64::new_unchecked(unpacked) };
-        FileId { id }
-    }
-}
diff --git a/crates/apollo-compiler/src/parser.rs b/crates/apollo-compiler/src/parser.rs
index 29f69ff..6433c09 100644
--- a/crates/apollo-compiler/src/parser.rs
+++ b/crates/apollo-compiler/src/parser.rs
@@ -1,20 +1,27 @@
+//! APIs related to parsing `&str` inputs as GraphQL syntax
+
 use crate::ast;
 use crate::ast::from_cst::Convert;
 use crate::ast::Document;
 use crate::collections::IndexMap;
 use crate::executable;
-use crate::execution::GraphQLLocation;
 use crate::schema::SchemaBuilder;
 use crate::validation::Details;
 use crate::validation::DiagnosticList;
-use crate::validation::FileId;
 use crate::validation::Valid;
 use crate::validation::WithErrors;
 use crate::ExecutableDocument;
-use crate::NodeLocation;
 use crate::Schema;
+use apollo_parser::SyntaxNode;
+use rowan::TextRange;
+use serde::Deserialize;
+use serde::Serialize;
+use std::num::NonZeroU64;
+use std::ops::Range;
 use std::path::Path;
 use std::path::PathBuf;
+use std::sync::atomic;
+use std::sync::atomic::AtomicU64;
 use std::sync::Arc;
 use std::sync::OnceLock;
 
@@ -35,8 +42,42 @@ pub struct SourceFile {
     pub(crate) source: OnceLock<ariadne::Source>,
 }
 
+/// A map of source files relevant to a given document
 pub type SourceMap = Arc<IndexMap<FileId, Arc<SourceFile>>>;
 
+/// Integer identifier for a parsed source file.
+///
+/// Used internally to support validating for example a schema built from multiple source files,
+/// and having diagnostics point to relevant sources.
+#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
+pub struct FileId {
+    id: NonZeroU64,
+}
+
+#[derive(Copy, Clone)]
+pub(crate) struct TaggedFileId {
+    tag_and_id: NonZeroU64,
+}
+
+/// The source location of a parsed node:
+/// file ID and text range (start and end byte offsets) within that file.
+#[derive(Clone, Copy, Hash, PartialEq, Eq)]
+pub struct SourceSpan {
+    pub(crate) file_id: FileId,
+    pub(crate) text_range: TextRange,
+}
+
+/// A line number and column number within a GraphQL document.
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct LineColumn {
+    /// The line number for this location, starting at 1 for the first line.
+    pub line: usize,
+    /// The column number for this location, starting at 1 and counting characters (Unicode Scalar
+    /// Values) like [`str::chars`].
+    pub column: usize,
+}
+
 /// Parse a schema and executable document from the given source text
 /// containing a mixture of type system definitions and executable definitions.
 /// and validate them.
@@ -136,7 +177,7 @@ impl Parser {
             let Ok(len) = parser_error.data().len().try_into() else {
                 continue;
             };
-            let location = Some(NodeLocation {
+            let location = Some(SourceSpan {
                 file_id,
                 text_range: rowan::TextRange::at(index, len),
             });
@@ -363,9 +404,9 @@ impl SourceFile {
         })
     }
 
-    pub(crate) fn get_line_column(&self, index: usize) -> Option<GraphQLLocation> {
+    pub(crate) fn get_line_column(&self, index: usize) -> Option<LineColumn> {
         let (_, zero_indexed_line, zero_indexed_column) = self.ariadne().get_byte_line(index)?;
-        Some(GraphQLLocation {
+        Some(LineColumn {
             line: zero_indexed_line + 1,
             column: zero_indexed_column + 1,
         })
@@ -392,3 +433,169 @@ impl std::fmt::Debug for SourceFile {
         debug_struct.finish()
     }
 }
+
+impl std::fmt::Debug for FileId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.id.fmt(f)
+    }
+}
+
+/// The next file ID to use. This is global so file IDs do not conflict between different compiler
+/// instances.
+static NEXT: AtomicU64 = AtomicU64::new(INITIAL);
+static INITIAL: u64 = 3;
+
+const TAG: u64 = 1 << 63;
+const ID_MASK: u64 = !TAG;
+
+#[allow(clippy::assertions_on_constants)]
+const _: () = {
+    assert!(TAG == 0x8000_0000_0000_0000);
+    assert!(ID_MASK == 0x7FFF_FFFF_FFFF_FFFF);
+};
+
+impl FileId {
+    /// The ID of the file implicitly added to type systems, for built-in scalars and introspection types
+    pub const BUILT_IN: Self = Self::const_new(1);
+
+    /// Passed to Ariadne to create a report without a location
+    pub(crate) const NONE: Self = Self::const_new(2);
+
+    // Returning a different value every time does not sound like good `impl Default`
+    #[allow(clippy::new_without_default)]
+    pub fn new() -> Self {
+        loop {
+            let id = NEXT.fetch_add(1, atomic::Ordering::AcqRel);
+            if id & TAG == 0 {
+                return Self {
+                    id: NonZeroU64::new(id).unwrap(),
+                };
+            } else {
+                // Overflowing 63 bits is unlikely, but if it somehow happens
+                // reset the counter and try again.
+                //
+                // `TaggedFileId` behaving incorrectly would be a memory safety issue,
+                // whereas a file ID collision “merely” causes
+                // diagnostics to print the wrong file name and source context.
+                Self::reset()
+            }
+        }
+    }
+
+    /// Reset file ID counter back to its initial value, used to get consistent results in tests.
+    ///
+    /// All tests in the process must use `#[serial_test::serial]`
+    #[doc(hidden)]
+    pub fn reset() {
+        NEXT.store(INITIAL, atomic::Ordering::Release)
+    }
+
+    const fn const_new(id: u64) -> Self {
+        assert!(id & ID_MASK == id);
+        // TODO: use unwrap() when const-stable https://github.com/rust-lang/rust/issues/67441
+        if let Some(id) = NonZeroU64::new(id) {
+            Self { id }
+        } else {
+            panic!()
+        }
+    }
+}
+
+impl TaggedFileId {
+    pub(crate) const fn pack(tag: bool, id: FileId) -> Self {
+        debug_assert!((id.id.get() & TAG) == 0);
+        let tag_and_id = if tag {
+            let packed = id.id.get() | TAG;
+            // SAFETY: `id.id` was non-zero, so setting an additional bit is still non-zero
+            unsafe { NonZeroU64::new_unchecked(packed) }
+        } else {
+            id.id
+        };
+        Self { tag_and_id }
+    }
+
+    pub(crate) fn tag(self) -> bool {
+        (self.tag_and_id.get() & TAG) != 0
+    }
+
+    pub(crate) fn file_id(self) -> FileId {
+        let unpacked = self.tag_and_id.get() & ID_MASK;
+        // SAFETY: `unpacked` has the same value as `id: FileId` did in `pack()`, which is non-zero
+        let id = unsafe { NonZeroU64::new_unchecked(unpacked) };
+        FileId { id }
+    }
+}
+
+impl SourceSpan {
+    pub(crate) fn new(file_id: FileId, node: &'_ SyntaxNode) -> Self {
+        Self {
+            file_id,
+            text_range: node.text_range(),
+        }
+    }
+
+    /// Returns the file ID for this location
+    pub fn file_id(&self) -> FileId {
+        self.file_id
+    }
+
+    /// Returns the offset from the start of the file to the start of the range, in UTF-8 bytes
+    pub fn offset(&self) -> usize {
+        self.text_range.start().into()
+    }
+
+    /// Returns the offset from the start of the file to the end of the range, in UTF-8 bytes
+    pub fn end_offset(&self) -> usize {
+        self.text_range.end().into()
+    }
+
+    /// Returns the length of the range, in UTF-8 bytes
+    pub fn node_len(&self) -> usize {
+        self.text_range.len().into()
+    }
+
+    /// Best effort at making a location with the given start and end
+    pub fn recompose(start_of: Option<Self>, end_of: Option<Self>) -> Option<Self> {
+        match (start_of, end_of) {
+            (None, None) => None,
+            (None, single @ Some(_)) | (single @ Some(_), None) => single,
+            (Some(start), Some(end)) => {
+                if start.file_id != end.file_id {
+                    // Pick one aribtrarily
+                    return Some(end);
+                }
+                Some(SourceSpan {
+                    file_id: start.file_id,
+                    text_range: TextRange::new(start.text_range.start(), end.text_range.end()),
+                })
+            }
+        }
+    }
+
+    /// The line and column numbers of [`Self::offset`]
+    pub fn line_column(&self, sources: &SourceMap) -> Option<LineColumn> {
+        let source = sources.get(&self.file_id)?;
+        source.get_line_column(self.offset())
+    }
+
+    /// The line and column numbers of the range from [`Self::offset`] to [`Self::end_offset`]
+    /// inclusive.
+    pub fn line_column_range(&self, sources: &SourceMap) -> Option<Range<LineColumn>> {
+        let source = sources.get(&self.file_id)?;
+        let start = source.get_line_column(self.offset())?;
+        let end = source.get_line_column(self.end_offset())?;
+        Some(Range { start, end })
+    }
+}
+
+impl std::fmt::Debug for SourceSpan {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}..{} @{:?}",
+            self.offset(),
+            self.end_offset(),
+            self.file_id,
+        )
+    }
+}
diff --git a/crates/apollo-compiler/src/schema/component.rs b/crates/apollo-compiler/src/schema/component.rs
index 5fd314e..491a65f 100644
--- a/crates/apollo-compiler/src/schema/component.rs
+++ b/crates/apollo-compiler/src/schema/component.rs
@@ -1,6 +1,6 @@
+use crate::parser::SourceSpan;
 use crate::Name;
 use crate::Node;
-use crate::NodeLocation;
 use std::fmt;
 use std::hash;
 use std::ops::Deref;
@@ -32,7 +32,7 @@ pub enum ComponentOrigin {
 /// even if they contain the same source location.
 #[derive(Debug, Clone, Eq)]
 pub struct ExtensionId {
-    arc: Arc<Option<NodeLocation>>,
+    arc: Arc<Option<SourceSpan>>,
 }
 
 impl ExtensionId {
@@ -42,7 +42,7 @@ impl ExtensionId {
         }
     }
 
-    pub fn location(&self) -> Option<NodeLocation> {
+    pub fn location(&self) -> Option<SourceSpan> {
         *self.arc
     }
 
diff --git a/crates/apollo-compiler/src/schema/mod.rs b/crates/apollo-compiler/src/schema/mod.rs
index 63a8b85..5807927 100644
--- a/crates/apollo-compiler/src/schema/mod.rs
+++ b/crates/apollo-compiler/src/schema/mod.rs
@@ -1,14 +1,20 @@
 //! High-level representation of a GraphQL schema
 
 use crate::ast;
+use crate::collections::HashMap;
+use crate::collections::HashSet;
 use crate::collections::IndexMap;
 use crate::collections::IndexSet;
-use crate::validation::FileId;
+use crate::name;
+use crate::parser::FileId;
+use crate::parser::Parser;
+use crate::parser::SourceSpan;
+use crate::ty;
+use crate::validation::DiagnosticList;
+use crate::validation::Valid;
+use crate::validation::WithErrors;
+pub use crate::Name;
 use crate::Node;
-use crate::NodeLocation;
-use crate::Parser;
-use std::collections::HashMap;
-use std::collections::HashSet;
 use std::path::Path;
 use std::sync::OnceLock;
 
@@ -31,12 +37,6 @@ pub use crate::ast::InputValueDefinition;
 pub use crate::ast::NamedType;
 pub use crate::ast::Type;
 pub use crate::ast::Value;
-use crate::name;
-use crate::ty;
-use crate::validation::DiagnosticList;
-use crate::validation::Valid;
-use crate::validation::WithErrors;
-pub use crate::Name;
 
 /// High-level representation of a GraphQL schema
 #[derive(Clone)]
@@ -44,7 +44,7 @@ pub struct Schema {
     /// Source files, if any, that were parsed to contribute to this schema.
     ///
     /// The schema (including parsed definitions) may have been modified since parsing.
-    pub sources: crate::SourceMap,
+    pub sources: crate::parser::SourceMap,
 
     /// The `schema` definition and its extensions, defining root operations
     pub schema_definition: Node<SchemaDefinition>,
@@ -192,18 +192,18 @@ pub(crate) enum BuildError {
 
     #[error("must not have multiple `schema` definitions")]
     SchemaDefinitionCollision {
-        previous_location: Option<NodeLocation>,
+        previous_location: Option<SourceSpan>,
     },
 
     #[error("the directive `@{name}` is defined multiple times in the schema")]
     DirectiveDefinitionCollision {
-        previous_location: Option<NodeLocation>,
+        previous_location: Option<SourceSpan>,
         name: Name,
     },
 
     #[error("the type `{name}` is defined multiple times in the schema")]
     TypeDefinitionCollision {
-        previous_location: Option<NodeLocation>,
+        previous_location: Option<SourceSpan>,
         name: Name,
     },
 
@@ -220,13 +220,13 @@ pub(crate) enum BuildError {
     TypeExtensionKindMismatch {
         name: Name,
         describe_ext: &'static str,
-        def_location: Option<NodeLocation>,
+        def_location: Option<SourceSpan>,
         describe_def: &'static str,
     },
 
     #[error("duplicate definitions for the `{operation_type}` root operation type")]
     DuplicateRootOperation {
-        previous_location: Option<NodeLocation>,
+        previous_location: Option<SourceSpan>,
         operation_type: &'static str,
     },
 
@@ -466,7 +466,7 @@ impl Schema {
     /// If that is repeated for multiple interfaces,
     /// gathering them all at once amorticizes that cost.
     pub fn implementers_map(&self) -> HashMap<Name, Implementers> {
-        let mut map = HashMap::<Name, Implementers>::new();
+        let mut map = HashMap::<Name, Implementers>::default();
         for (ty_name, ty) in &self.types {
             match ty {
                 ExtendedType::Object(def) => {
@@ -552,6 +552,18 @@ impl Schema {
 }
 
 impl SchemaDefinition {
+    pub fn iter_root_operations(
+        &self,
+    ) -> impl Iterator<Item = (ast::OperationType, &ComponentName)> {
+        [
+            (ast::OperationType::Query, &self.query),
+            (ast::OperationType::Mutation, &self.mutation),
+            (ast::OperationType::Subscription, &self.subscription),
+        ]
+        .into_iter()
+        .filter_map(|(ty, maybe_op)| maybe_op.as_ref().map(|op| (ty, op)))
+    }
+
     /// Collect `schema` extensions that contribute any component
     ///
     /// The order of the returned set is unspecified but deterministic
@@ -594,7 +606,7 @@ impl ExtendedType {
     /// Return the source location of the type's base definition.
     ///
     /// If the type has extensions, those are not covered by this location.
-    pub fn location(&self) -> Option<NodeLocation> {
+    pub fn location(&self) -> Option<SourceSpan> {
         match self {
             Self::Scalar(ty) => ty.location(),
             Self::Object(ty) => ty.location(),
@@ -640,6 +652,14 @@ impl ExtendedType {
         matches!(self, Self::InputObject(_))
     }
 
+    /// Returns wether this type is a leaf type: scalar or enum.
+    ///
+    /// Field selections must have sub-selections if and only if
+    /// their inner named type is *not* a leaf field.
+    pub fn is_leaf(&self) -> bool {
+        matches!(self, Self::Scalar(_) | Self::Enum(_))
+    }
+
     /// Returns true if a value of this type can be used as an input value.
     ///
     /// # Spec
@@ -924,11 +944,11 @@ impl PartialEq for Schema {
     fn eq(&self, other: &Self) -> bool {
         let Self {
             sources: _, // ignored
-            schema_definition: root_operations,
+            schema_definition,
             directive_definitions,
             types,
         } = self;
-        *root_operations == other.schema_definition
+        *schema_definition == other.schema_definition
             && *directive_definitions == other.directive_definitions
             && *types == other.types
     }
diff --git a/crates/apollo-compiler/src/schema/serialize.rs b/crates/apollo-compiler/src/schema/serialize.rs
index aa1e315..c63cb79 100644
--- a/crates/apollo-compiler/src/schema/serialize.rs
+++ b/crates/apollo-compiler/src/schema/serialize.rs
@@ -73,15 +73,9 @@ impl Node<SchemaDefinition> {
                 .into_iter()
                 .any(|op| op.is_some());
         let root_ops = |ext: Option<&ExtensionId>| -> Vec<Node<(OperationType, Name)>> {
-            let root_op = |op: &Option<ComponentName>, ty| {
-                op.as_ref()
-                    .filter(|name| name.origin.extension_id() == ext)
-                    .map(|name| (ty, name.name.clone()).into())
-                    .into_iter()
-            };
-            root_op(&self.query, OperationType::Query)
-                .chain(root_op(&self.mutation, OperationType::Mutation))
-                .chain(root_op(&self.subscription, OperationType::Subscription))
+            self.iter_root_operations()
+                .filter(|(_, op)| op.origin.extension_id() == ext)
+                .map(|(ty, op)| (ty, op.name.clone()).into())
                 .collect()
         };
         if implict {
@@ -132,7 +126,7 @@ impl ExtendedType {
 }
 
 impl ScalarType {
-    fn to_ast(&self, location: Option<NodeLocation>) -> impl Iterator<Item = ast::Definition> + '_ {
+    fn to_ast(&self, location: Option<SourceSpan>) -> impl Iterator<Item = ast::Definition> + '_ {
         let def = ast::ScalarTypeDefinition {
             description: self.description.clone(),
             name: self.name.clone(),
@@ -156,7 +150,7 @@ impl ScalarType {
 }
 
 impl ObjectType {
-    fn to_ast(&self, location: Option<NodeLocation>) -> impl Iterator<Item = ast::Definition> + '_ {
+    fn to_ast(&self, location: Option<SourceSpan>) -> impl Iterator<Item = ast::Definition> + '_ {
         let def = ast::ObjectTypeDefinition {
             description: self.description.clone(),
             name: self.name.clone(),
@@ -184,7 +178,7 @@ impl ObjectType {
 }
 
 impl InterfaceType {
-    fn to_ast(&self, location: Option<NodeLocation>) -> impl Iterator<Item = ast::Definition> + '_ {
+    fn to_ast(&self, location: Option<SourceSpan>) -> impl Iterator<Item = ast::Definition> + '_ {
         let def = ast::InterfaceTypeDefinition {
             description: self.description.clone(),
             name: self.name.clone(),
@@ -214,7 +208,7 @@ impl InterfaceType {
 }
 
 impl UnionType {
-    fn to_ast(&self, location: Option<NodeLocation>) -> impl Iterator<Item = ast::Definition> + '_ {
+    fn to_ast(&self, location: Option<SourceSpan>) -> impl Iterator<Item = ast::Definition> + '_ {
         let def = ast::UnionTypeDefinition {
             description: self.description.clone(),
             name: self.name.clone(),
@@ -240,7 +234,7 @@ impl UnionType {
 }
 
 impl EnumType {
-    fn to_ast(&self, location: Option<NodeLocation>) -> impl Iterator<Item = ast::Definition> + '_ {
+    fn to_ast(&self, location: Option<SourceSpan>) -> impl Iterator<Item = ast::Definition> + '_ {
         let def = ast::EnumTypeDefinition {
             description: self.description.clone(),
             name: self.name.clone(),
@@ -266,7 +260,7 @@ impl EnumType {
 }
 
 impl InputObjectType {
-    fn to_ast(&self, location: Option<NodeLocation>) -> impl Iterator<Item = ast::Definition> + '_ {
+    fn to_ast(&self, location: Option<SourceSpan>) -> impl Iterator<Item = ast::Definition> + '_ {
         let def = ast::InputObjectTypeDefinition {
             description: self.description.clone(),
             name: self.name.clone(),
diff --git a/crates/apollo-compiler/src/validation/argument.rs b/crates/apollo-compiler/src/validation/argument.rs
index fd17982..1cb9b93 100644
--- a/crates/apollo-compiler/src/validation/argument.rs
+++ b/crates/apollo-compiler/src/validation/argument.rs
@@ -1,15 +1,15 @@
 use crate::ast;
+use crate::collections::HashMap;
 use crate::validation::diagnostics::DiagnosticData;
 use crate::validation::DiagnosticList;
-use crate::validation::NodeLocation;
+use crate::validation::SourceSpan;
 use crate::Node;
-use std::collections::HashMap;
 
 pub(crate) fn validate_arguments(
     diagnostics: &mut DiagnosticList,
     arguments: &[Node<ast::Argument>],
 ) {
-    let mut seen = HashMap::<_, Option<NodeLocation>>::new();
+    let mut seen = HashMap::<_, Option<SourceSpan>>::default();
 
     for argument in arguments {
         let name = &argument.name;
diff --git a/crates/apollo-compiler/src/validation/diagnostics.rs b/crates/apollo-compiler/src/validation/diagnostics.rs
index 9546fa2..ab74ac2 100644
--- a/crates/apollo-compiler/src/validation/diagnostics.rs
+++ b/crates/apollo-compiler/src/validation/diagnostics.rs
@@ -5,9 +5,9 @@ use crate::coordinate::SchemaCoordinate;
 use crate::coordinate::TypeAttributeCoordinate;
 use crate::diagnostic::CliReport;
 use crate::executable;
+use crate::parser::SourceSpan;
 use crate::Name;
 use crate::Node;
-use crate::NodeLocation;
 use std::fmt;
 use thiserror::Error;
 
@@ -18,26 +18,26 @@ pub(crate) enum DiagnosticData {
     #[error("the variable `${name}` is declared multiple times")]
     UniqueVariable {
         name: Name,
-        original_definition: Option<NodeLocation>,
-        redefined_definition: Option<NodeLocation>,
+        original_definition: Option<SourceSpan>,
+        redefined_definition: Option<SourceSpan>,
     },
     #[error("the argument `{name}` is provided multiple times")]
     UniqueArgument {
         name: Name,
-        original_definition: Option<NodeLocation>,
-        redefined_definition: Option<NodeLocation>,
+        original_definition: Option<SourceSpan>,
+        redefined_definition: Option<SourceSpan>,
     },
     #[error("the value `{name}` is defined multiple times")]
     UniqueInputValue {
         name: Name,
-        original_definition: Option<NodeLocation>,
-        redefined_definition: Option<NodeLocation>,
+        original_definition: Option<SourceSpan>,
+        redefined_definition: Option<SourceSpan>,
     },
     #[error("the argument `{name}` is not supported by `{coordinate}`")]
     UndefinedArgument {
         name: Name,
         coordinate: SchemaCoordinate,
-        definition_location: Option<NodeLocation>,
+        definition_location: Option<SourceSpan>,
     },
     #[error("cannot find type `{name}` in this document")]
     UndefinedDefinition {
@@ -65,7 +65,7 @@ pub(crate) enum DiagnosticData {
         value: Name,
         /// Name of the enum
         definition: Name,
-        definition_location: Option<NodeLocation>,
+        definition_location: Option<SourceSpan>,
     },
     #[error("field `{value}` does not exist on `{definition}`")]
     UndefinedInputValue {
@@ -73,31 +73,31 @@ pub(crate) enum DiagnosticData {
         value: Name,
         /// Name of the input object type
         definition: Name,
-        definition_location: Option<NodeLocation>,
+        definition_location: Option<SourceSpan>,
     },
     #[error("type `{name}` does not satisfy interface `{interface}`: missing field `{field}`")]
     MissingInterfaceField {
         name: Name,
         /// Location of the `implements XYZ` of the interface
-        implements_location: Option<NodeLocation>,
+        implements_location: Option<SourceSpan>,
         interface: Name,
         field: Name,
         /// Location of the definition of the field in the interface
-        field_location: Option<NodeLocation>,
+        field_location: Option<SourceSpan>,
     },
     #[error("the required argument `{coordinate}` is not provided")]
     RequiredArgument {
         name: Name,
         coordinate: SchemaCoordinate,
         expected_type: Node<Type>,
-        definition_location: Option<NodeLocation>,
+        definition_location: Option<SourceSpan>,
     },
     #[error("the required field `{coordinate}` is not provided")]
     RequiredField {
         name: Name,
         coordinate: TypeAttributeCoordinate,
         expected_type: Node<Type>,
-        definition_location: Option<NodeLocation>,
+        definition_location: Option<SourceSpan>,
     },
     #[error(
         "interface `{interface}` declares that it implements `{via_interface}`, but to do so it must also implement `{missing_interface}`"
@@ -108,7 +108,7 @@ pub(crate) enum DiagnosticData {
         /// Super interface that declares the implementation
         via_interface: Name,
         /// Source location where the super interface declares the implementation
-        transitive_interface_location: Option<NodeLocation>,
+        transitive_interface_location: Option<SourceSpan>,
         /// Interface that should be implemented
         missing_interface: Name,
     },
@@ -118,7 +118,7 @@ pub(crate) enum DiagnosticData {
         name: Name,
         /// The kind of type that the field is declared with.
         describe_type: &'static str,
-        type_location: Option<NodeLocation>,
+        type_location: Option<SourceSpan>,
     },
     #[error("`{name}` field must be of an input type")]
     InputType {
@@ -126,7 +126,7 @@ pub(crate) enum DiagnosticData {
         name: Name,
         /// The kind of type that the field is declared with.
         describe_type: &'static str,
-        type_location: Option<NodeLocation>,
+        type_location: Option<SourceSpan>,
     },
     #[error("`${name}` variable must be of an input type")]
     VariableInputType {
@@ -163,7 +163,7 @@ pub(crate) enum DiagnosticData {
         /// Locations that *are* valid for this directive
         valid_locations: Vec<DirectiveLocation>,
         /// The source location where the directive that's being used was defined.
-        definition_location: Option<NodeLocation>,
+        definition_location: Option<SourceSpan>,
     },
     #[error("expected value of type {ty}, found {}", .value.describe())]
     UnsupportedValueType {
@@ -171,7 +171,7 @@ pub(crate) enum DiagnosticData {
         value: Node<ast::Value>,
         /// Expected concrete type
         ty: Node<Type>,
-        definition_location: Option<NodeLocation>,
+        definition_location: Option<SourceSpan>,
     },
     #[error("int cannot represent non 32-bit signed integer value")]
     IntCoercionError {
@@ -187,7 +187,7 @@ pub(crate) enum DiagnosticData {
     UniqueDirective {
         /// Name of the non-unique directive.
         name: Name,
-        original_application: Option<NodeLocation>,
+        original_application: Option<SourceSpan>,
     },
     #[error("interface, union and object types must have a subselection set")]
     MissingSubselection {
@@ -216,9 +216,9 @@ pub(crate) enum DiagnosticData {
         type_name: Name,
         type_condition: Name,
         /// Source location where the fragment is defined
-        fragment_location: Option<NodeLocation>,
+        fragment_location: Option<SourceSpan>,
         /// Source location of the type the fragment is being applied to.
-        type_location: Option<NodeLocation>,
+        type_location: Option<SourceSpan>,
     },
     #[error("fragment `{name}` must be used in an operation")]
     UnusedFragment {
@@ -232,11 +232,11 @@ pub(crate) enum DiagnosticData {
         /// Name of the variable being used in an argument
         variable: Name,
         variable_type: Type,
-        variable_location: Option<NodeLocation>,
+        variable_location: Option<SourceSpan>,
         /// Name of the argument where variable is used
         argument: Name,
         argument_type: Type,
-        argument_location: Option<NodeLocation>,
+        argument_location: Option<SourceSpan>,
     },
     #[error("`{name}` directive definition cannot reference itself")]
     RecursiveDirectiveDefinition {
@@ -253,7 +253,7 @@ pub(crate) enum DiagnosticData {
     #[error("`{name}` fragment cannot reference itself")]
     RecursiveFragmentDefinition {
         /// Source location of just the "fragment FragName" part.
-        head_location: Option<NodeLocation>,
+        head_location: Option<SourceSpan>,
         name: Name,
         trace: Vec<Node<executable::FragmentSpread>>,
     },
@@ -267,31 +267,31 @@ pub(crate) enum DiagnosticData {
     #[error("`{type_name}` has no fields")]
     EmptyFieldSet {
         type_name: Name,
-        type_location: Option<NodeLocation>,
-        extensions_locations: Vec<Option<NodeLocation>>,
+        type_location: Option<SourceSpan>,
+        extensions_locations: Vec<Option<SourceSpan>>,
     },
     #[error("`{type_name}` has no enum values")]
     EmptyValueSet {
         type_name: Name,
-        type_location: Option<NodeLocation>,
-        extensions_locations: Vec<Option<NodeLocation>>,
+        type_location: Option<SourceSpan>,
+        extensions_locations: Vec<Option<SourceSpan>>,
     },
     #[error("`{type_name}` has no member types")]
     EmptyMemberSet {
         type_name: Name,
-        type_location: Option<NodeLocation>,
-        extensions_locations: Vec<Option<NodeLocation>>,
+        type_location: Option<SourceSpan>,
+        extensions_locations: Vec<Option<SourceSpan>>,
     },
     #[error("`{type_name}` has no input values")]
     EmptyInputValueSet {
         type_name: Name,
-        type_location: Option<NodeLocation>,
-        extensions_locations: Vec<Option<NodeLocation>>,
+        type_location: Option<SourceSpan>,
+        extensions_locations: Vec<Option<SourceSpan>>,
     },
 }
 
 impl DiagnosticData {
-    pub(crate) fn report(&self, main_location: Option<NodeLocation>, report: &mut CliReport) {
+    pub(crate) fn report(&self, main_location: Option<SourceSpan>, report: &mut CliReport) {
         match self {
             DiagnosticData::UniqueVariable {
                 name,
@@ -720,8 +720,8 @@ impl DiagnosticData {
     fn report_empty_type(
         report: &mut CliReport,
         type_name: &Name,
-        type_location: &Option<NodeLocation>,
-        extensions_locations: &[Option<NodeLocation>],
+        type_location: &Option<SourceSpan>,
+        extensions_locations: &[Option<SourceSpan>],
         describe_missing_kind: &str,
     ) {
         report.with_label_opt(
diff --git a/crates/apollo-compiler/src/validation/directive.rs b/crates/apollo-compiler/src/validation/directive.rs
index 6290ed2..8982afa 100644
--- a/crates/apollo-compiler/src/validation/directive.rs
+++ b/crates/apollo-compiler/src/validation/directive.rs
@@ -1,16 +1,16 @@
 use super::CycleError;
 use crate::ast;
+use crate::collections::HashMap;
+use crate::collections::HashSet;
 use crate::coordinate::DirectiveArgumentCoordinate;
 use crate::coordinate::DirectiveCoordinate;
 use crate::schema;
 use crate::validation::diagnostics::DiagnosticData;
 use crate::validation::DiagnosticList;
-use crate::validation::NodeLocation;
 use crate::validation::RecursionGuard;
 use crate::validation::RecursionStack;
+use crate::validation::SourceSpan;
 use crate::Node;
-use std::collections::HashMap;
-use std::collections::HashSet;
 
 /// This struct just groups functions that are used to find self-referential directives.
 /// The way to use it is to call `FindRecursiveDirective::check`.
@@ -149,7 +149,7 @@ pub(crate) fn validate_directive_definition(
         ast::DirectiveLocation::ArgumentDefinition,
     );
 
-    let head_location = NodeLocation::recompose(def.location(), def.name.location());
+    let head_location = SourceSpan::recompose(def.location(), def.name.location());
 
     // A directive definition must not contain the use of a directive which
     // references itself directly.
@@ -194,7 +194,7 @@ pub(crate) fn validate_directives<'dir>(
     dir_loc: ast::DirectiveLocation,
     var_defs: &[Node<ast::VariableDefinition>],
 ) {
-    let mut seen_directives = HashMap::<_, Option<NodeLocation>>::new();
+    let mut seen_directives = HashMap::<_, Option<SourceSpan>>::default();
 
     for dir in dirs {
         super::argument::validate_arguments(diagnostics, &dir.arguments);
@@ -220,7 +220,7 @@ pub(crate) fn validate_directives<'dir>(
                 );
             }
         } else {
-            let loc = NodeLocation::recompose(dir.location(), dir.name.location());
+            let loc = SourceSpan::recompose(dir.location(), dir.name.location());
             seen_directives.insert(&dir.name, loc);
         }
 
diff --git a/crates/apollo-compiler/src/validation/fragment.rs b/crates/apollo-compiler/src/validation/fragment.rs
index f0c581c..92f0f78 100644
--- a/crates/apollo-compiler/src/validation/fragment.rs
+++ b/crates/apollo-compiler/src/validation/fragment.rs
@@ -1,21 +1,21 @@
 use crate::ast;
 use crate::ast::NamedType;
+use crate::collections::HashMap;
+use crate::collections::HashSet;
 use crate::executable;
 use crate::schema;
 use crate::schema::Implementers;
 use crate::validation::diagnostics::DiagnosticData;
 use crate::validation::CycleError;
 use crate::validation::DiagnosticList;
-use crate::validation::NodeLocation;
 use crate::validation::OperationValidationContext;
 use crate::validation::RecursionGuard;
 use crate::validation::RecursionStack;
+use crate::validation::SourceSpan;
 use crate::ExecutableDocument;
 use crate::Name;
 use crate::Node;
 use std::borrow::Cow;
-use std::collections::HashMap;
-use std::collections::HashSet;
 
 /// Given a type definition, find all the type names that can be used for fragment spreading.
 ///
@@ -27,7 +27,7 @@ fn get_possible_types<'a>(
     match type_definition {
         // 1. If `type` is an object type, return a set containing `type`.
         schema::ExtendedType::Object(object) => {
-            let mut set = HashSet::new();
+            let mut set = HashSet::default();
             set.insert(object.name.clone());
             Cow::Owned(set)
         }
@@ -299,7 +299,7 @@ pub(crate) fn validate_fragment_cycles(
     match detect_fragment_cycles(document, &def.selection_set, &mut visited.guard()) {
         Ok(_) => {}
         Err(CycleError::Recursed(trace)) => {
-            let head_location = NodeLocation::recompose(def.location(), def.name.location());
+            let head_location = SourceSpan::recompose(def.location(), def.name.location());
 
             diagnostics.push(
                 def.location(),
@@ -311,7 +311,7 @@ pub(crate) fn validate_fragment_cycles(
             );
         }
         Err(CycleError::Limit(_)) => {
-            let head_location = NodeLocation::recompose(def.location(), def.name.location());
+            let head_location = SourceSpan::recompose(def.location(), def.name.location());
 
             diagnostics.push(
                 head_location,
@@ -329,7 +329,7 @@ pub(crate) fn validate_fragment_type_condition(
     schema: &crate::Schema,
     fragment_name: Option<Name>,
     type_cond: &NamedType,
-    fragment_location: Option<NodeLocation>,
+    fragment_location: Option<SourceSpan>,
 ) {
     let type_def = schema.types.get(type_cond);
     let is_composite = type_def
diff --git a/crates/apollo-compiler/src/validation/input_object.rs b/crates/apollo-compiler/src/validation/input_object.rs
index 90fb6de..1faec6c 100644
--- a/crates/apollo-compiler/src/validation/input_object.rs
+++ b/crates/apollo-compiler/src/validation/input_object.rs
@@ -1,4 +1,5 @@
 use crate::ast;
+use crate::collections::HashMap;
 use crate::schema::ExtendedType;
 use crate::schema::InputObjectType;
 use crate::validation::diagnostics::DiagnosticData;
@@ -8,7 +9,6 @@ use crate::validation::RecursionGuard;
 use crate::validation::RecursionStack;
 use crate::Name;
 use crate::Node;
-use std::collections::HashMap;
 
 // Implements [Circular References](https://spec.graphql.org/October2021/#sec-Input-Objects.Circular-References)
 // part of the input object validation spec.
@@ -151,7 +151,7 @@ pub(crate) fn validate_argument_definitions(
 ) {
     validate_input_value_definitions(diagnostics, schema, input_values, directive_location);
 
-    let mut seen: HashMap<Name, &Node<ast::InputValueDefinition>> = HashMap::new();
+    let mut seen: HashMap<Name, &Node<ast::InputValueDefinition>> = HashMap::default();
     for input_value in input_values {
         let name = &input_value.name;
         if let Some(prev_value) = seen.get(name) {
diff --git a/crates/apollo-compiler/src/validation/interface.rs b/crates/apollo-compiler/src/validation/interface.rs
index f9c79b1..17cb2e4 100644
--- a/crates/apollo-compiler/src/validation/interface.rs
+++ b/crates/apollo-compiler/src/validation/interface.rs
@@ -1,5 +1,6 @@
 use crate::ast;
 use crate::collections::IndexSet;
+use crate::parser::SourceSpan;
 use crate::schema::ComponentName;
 use crate::schema::ExtendedType;
 use crate::schema::InterfaceType;
@@ -8,7 +9,6 @@ use crate::validation::diagnostics::DiagnosticData;
 use crate::validation::field::validate_field_definitions;
 use crate::validation::DiagnosticList;
 use crate::Node;
-use crate::NodeLocation;
 
 pub(crate) fn validate_interface_definitions(
     diagnostics: &mut DiagnosticList,
@@ -120,7 +120,7 @@ pub(crate) fn validate_implements_interfaces(
     diagnostics: &mut DiagnosticList,
     schema: &crate::Schema,
     implementor_name: &Name,
-    implementor_location: Option<NodeLocation>,
+    implementor_location: Option<SourceSpan>,
     implements_interfaces: &IndexSet<ComponentName>,
 ) {
     let interface_definitions = implements_interfaces
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index 16db70e..0d9024a 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -23,6 +23,7 @@ pub(crate) mod union_;
 pub(crate) mod value;
 pub(crate) mod variable;
 
+use crate::collections::HashMap;
 use crate::collections::IndexSet;
 use crate::diagnostic::CliReport;
 use crate::diagnostic::Diagnostic;
@@ -34,14 +35,12 @@ use crate::executable::ConflictingFieldType;
 use crate::executable::VariableDefinition;
 use crate::execution::GraphQLError;
 use crate::execution::Response;
-pub(crate) use crate::node::FileId;
+use crate::parser::SourceMap;
+use crate::parser::SourceSpan;
 use crate::schema::BuildError as SchemaBuildError;
 use crate::schema::Implementers;
 use crate::Name;
 use crate::Node;
-use crate::NodeLocation;
-use crate::SourceMap;
-use std::collections::HashMap;
 use std::fmt;
 use std::sync::Arc;
 use std::sync::OnceLock;
@@ -228,7 +227,7 @@ impl<T> fmt::Display for WithErrors<T> {
 #[derive(Debug, Clone)]
 pub struct SuspectedValidationBug {
     pub message: String,
-    pub location: Option<NodeLocation>,
+    pub location: Option<SourceSpan>,
 }
 
 impl SuspectedValidationBug {
@@ -261,7 +260,7 @@ pub struct DiagnosticList {
 #[derive(thiserror::Error, Debug, Clone)]
 #[error("{details}")]
 pub struct DiagnosticData {
-    location: Option<NodeLocation>,
+    location: Option<SourceSpan>,
     details: Details,
 }
 
@@ -639,7 +638,7 @@ impl DiagnosticData {
 }
 
 impl ToCliReport for DiagnosticData {
-    fn location(&self) -> Option<NodeLocation> {
+    fn location(&self) -> Option<SourceSpan> {
         self.location
     }
 
@@ -992,7 +991,7 @@ impl DiagnosticList {
             .map(|data| data.to_diagnostic(&self.sources))
     }
 
-    pub(crate) fn push(&mut self, location: Option<NodeLocation>, details: impl Into<Details>) {
+    pub(crate) fn push(&mut self, location: Option<SourceSpan>, details: impl Into<Details>) {
         self.diagnostics_data.push(DiagnosticData {
             location,
             details: details.into(),
diff --git a/crates/apollo-compiler/src/validation/selection.rs b/crates/apollo-compiler/src/validation/selection.rs
index ec5fbe0..92ec784 100644
--- a/crates/apollo-compiler/src/validation/selection.rs
+++ b/crates/apollo-compiler/src/validation/selection.rs
@@ -1,5 +1,7 @@
 use crate::ast;
 use crate::ast::NamedType;
+use crate::collections::HashMap;
+use crate::collections::HashSet;
 use crate::collections::IndexMap;
 use crate::coordinate::TypeAttributeCoordinate;
 use crate::executable;
@@ -16,8 +18,6 @@ use crate::Name;
 use crate::Node;
 use apollo_parser::LimitTracker;
 use std::cell::OnceCell;
-use std::collections::HashMap;
-use std::collections::HashSet;
 use std::collections::VecDeque;
 use std::hash::Hash;
 use std::rc::Rc;
@@ -495,11 +495,7 @@ pub(crate) struct FieldsInSetCanMerge<'alloc, 's, 'doc> {
     ///
     /// The value is an Rc because it needs to have an independent lifetime from `self`,
     /// so the cache can be updated while a field set is borrowed.
-    cache: HashMap<
-        &'alloc [FieldSelection<'doc>],
-        Rc<MergedFieldSet<'alloc, 'doc>>,
-        ahash::RandomState,
-    >,
+    cache: HashMap<&'alloc [FieldSelection<'doc>], Rc<MergedFieldSet<'alloc, 'doc>>>,
     // The recursion limit is used for two separate recursions, but they are not interleaved,
     // so the effective limit does apply to field nesting levels in both cases.
     recursion_limit: LimitTracker,
diff --git a/crates/apollo-compiler/src/validation/variable.rs b/crates/apollo-compiler/src/validation/variable.rs
index c6d9082..5cf0d0c 100644
--- a/crates/apollo-compiler/src/validation/variable.rs
+++ b/crates/apollo-compiler/src/validation/variable.rs
@@ -1,24 +1,24 @@
 use crate::ast;
+use crate::collections::HashMap;
+use crate::collections::HashSet;
 use crate::executable;
 use crate::validation::diagnostics::DiagnosticData;
 use crate::validation::DiagnosticList;
-use crate::validation::NodeLocation;
 use crate::validation::RecursionGuard;
 use crate::validation::RecursionLimitError;
 use crate::validation::RecursionStack;
+use crate::validation::SourceSpan;
 use crate::ExecutableDocument;
 use crate::Name;
 use crate::Node;
 use std::collections::hash_map::Entry;
-use std::collections::HashMap;
-use std::collections::HashSet;
 
 pub(crate) fn validate_variable_definitions(
     diagnostics: &mut DiagnosticList,
     schema: Option<&crate::Schema>,
     variables: &[Node<ast::VariableDefinition>],
 ) {
-    let mut seen: HashMap<Name, &Node<ast::VariableDefinition>> = HashMap::new();
+    let mut seen: HashMap<Name, &Node<ast::VariableDefinition>> = HashMap::default();
     for variable in variables.iter() {
         super::directive::validate_directives(
             diagnostics,
@@ -174,11 +174,11 @@ pub(crate) fn validate_unused_variables(
         .map(|var| {
             (
                 &var.name,
-                NodeLocation::recompose(var.location(), var.name.location()),
+                SourceSpan::recompose(var.location(), var.name.location()),
             )
         })
         .collect();
-    let mut used_vars = HashSet::new();
+    let mut used_vars = HashSet::default();
     let walked = walk_selections(
         document,
         &operation.selection_set,
diff --git a/crates/apollo-smith/CHANGELOG.md b/crates/apollo-smith/CHANGELOG.md
index 7c4fbaf..c41aef1 100644
--- a/crates/apollo-smith/CHANGELOG.md
+++ b/crates/apollo-smith/CHANGELOG.md
@@ -18,6 +18,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 ## Maintenance
 
 ## Documentation -->
+# [0.9.0](https://crates.io/crates/apollo-smith/0.9.0) - 2024-07-19
+
+- **Update apollo-compiler dependency to `=1.0.0-beta.19`**
+
 # [0.8.0](https://crates.io/crates/apollo-smith/0.8.0) - 2024-06-27
 
 - **Update apollo-compiler dependency to `=1.0.0-beta.18`**
diff --git a/crates/apollo-smith/Cargo.toml b/crates/apollo-smith/Cargo.toml
index 51c7cc3..5963efb 100644
--- a/crates/apollo-smith/Cargo.toml
+++ b/crates/apollo-smith/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "apollo-smith"
-version = "0.8.0" # When bumping, also update README.md
+version = "0.9.0" # When bumping, also update README.md
 edition = "2021"
 authors = ["Benjamin Coenen <benjamin.coenen@apollographql.com>"]
 license = "MIT OR Apache-2.0"
@@ -22,7 +22,7 @@ categories = [
 ]
 
 [dependencies]
-apollo-compiler = { path = "../apollo-compiler", version = "=1.0.0-beta.18" }
+apollo-compiler = { path = "../apollo-compiler", version = "=1.0.0-beta.19" }
 apollo-parser = { path = "../apollo-parser", version = "0.7.0" }
 arbitrary = { version = "1.3.0", features = ["derive"] }
 indexmap = "2.0.0"
diff --git a/crates/apollo-smith/README.md b/crates/apollo-smith/README.md
index 6a2076b..2b7f7ba 100644
--- a/crates/apollo-smith/README.md
+++ b/crates/apollo-smith/README.md
@@ -49,7 +49,7 @@ and add `apollo-smith` to your Cargo.toml:
 ## fuzz/Cargo.toml
 
 [dependencies]
-apollo-smith = "0.8.0"
+apollo-smith = "0.9.0"
 ```
 
 It can then be used in a `fuzz_target` along with the [`arbitrary`] crate,


Test patch:
diff --git a/crates/apollo-compiler/tests/introspection_split.rs b/crates/apollo-compiler/tests/introspection_split.rs
index eb954b0..4cb7aeb 100644
--- a/crates/apollo-compiler/tests/introspection_split.rs
+++ b/crates/apollo-compiler/tests/introspection_split.rs
@@ -78,7 +78,7 @@ fn test_nested() {
         GraphQLError {
             message: "Schema introspection field __schema is not supported nested in other fields",
             locations: [
-                GraphQLLocation {
+                LineColumn {
                     line: 2,
                     column: 43,
                 },
@@ -100,7 +100,7 @@ fn test_nested_in_fragment() {
         GraphQLError {
             message: "Schema introspection field __schema is not supported nested in other fields",
             locations: [
-                GraphQLLocation {
+                LineColumn {
                     line: 3,
                     column: 41,
                 },
@@ -121,7 +121,7 @@ fn test_mutation() {
         GraphQLError {
             message: "Schema introspection field __schema is not supported in a mutation operation",
             locations: [
-                GraphQLLocation {
+                LineColumn {
                     line: 2,
                     column: 33,
                 },
@@ -142,7 +142,7 @@ fn test_mutation_nested() {
         GraphQLError {
             message: "Schema introspection field __schema is not supported in a mutation operation",
             locations: [
-                GraphQLLocation {
+                LineColumn {
                     line: 2,
                     column: 46,
                 },
diff --git a/crates/apollo-compiler/tests/parser.rs b/crates/apollo-compiler/tests/parser.rs
index 5f3278b..ed89767 100644
--- a/crates/apollo-compiler/tests/parser.rs
+++ b/crates/apollo-compiler/tests/parser.rs
@@ -1,4 +1,4 @@
-use apollo_compiler::Parser;
+use apollo_compiler::parser::Parser;
 
 #[test]
 fn it_errors_when_selection_set_recursion_limit_exceeded() {
diff --git a/crates/apollo-compiler/tests/snapshot_tests.rs b/crates/apollo-compiler/tests/snapshot_tests.rs
index 4bd5f82..6f63881 100644
--- a/crates/apollo-compiler/tests/snapshot_tests.rs
+++ b/crates/apollo-compiler/tests/snapshot_tests.rs
@@ -10,10 +10,10 @@
 use apollo_compiler::ast;
 use apollo_compiler::collections::IndexMap;
 use apollo_compiler::name;
+use apollo_compiler::parser::FileId;
 use apollo_compiler::schema;
 use apollo_compiler::ty;
 use apollo_compiler::validation::DiagnosticList;
-use apollo_compiler::FileId;
 use apollo_compiler::Schema;
 use expect_test::expect_file;
 use serial_test::serial;
diff --git a/crates/apollo-compiler/tests/validation/mod.rs b/crates/apollo-compiler/tests/validation/mod.rs
index b6d7ffe..d302b65 100644
--- a/crates/apollo-compiler/tests/validation/mod.rs
+++ b/crates/apollo-compiler/tests/validation/mod.rs
@@ -7,7 +7,7 @@ mod types;
 mod variable;
 
 use apollo_compiler::ast;
-use apollo_compiler::execution::GraphQLLocation;
+use apollo_compiler::parser::LineColumn;
 use apollo_compiler::ExecutableDocument;
 use apollo_compiler::Schema;
 use std::ops::Range;
@@ -73,8 +73,8 @@ query {
         assert_eq!(
             diag.line_column_range(),
             Some(Range {
-                start: GraphQLLocation { line: 2, column: 1 },
-                end: GraphQLLocation { line: 4, column: 2 }
+                start: LineColumn { line: 2, column: 1 },
+                end: LineColumn { line: 4, column: 2 }
             })
         );
         json.assert_eq(&serde_json::to_string_pretty(&diag.to_json()).unwrap());
@@ -157,8 +157,8 @@ fragment q on TestObject {
         assert_eq!(
             diag.line_column_range(),
             Some(Range {
-                start: GraphQLLocation { line: 8, column: 1 },
-                end: GraphQLLocation {
+                start: LineColumn { line: 8, column: 1 },
+                end: LineColumn {
                     line: 10,
                     column: 2
                 }
@@ -202,11 +202,11 @@ fn validation_without_type_system() {
         assert_eq!(
             diag.line_column_range(),
             Some(Range {
-                start: GraphQLLocation {
+                start: LineColumn {
                     line: 2,
                     column: 13,
                 },
-                end: GraphQLLocation {
+                end: LineColumn {
                     line: 2,
                     column: 37,
                 }
@@ -244,11 +244,11 @@ fn validation_without_type_system() {
         assert_eq!(
             diag.line_column_range(),
             Some(Range {
-                start: GraphQLLocation {
+                start: LineColumn {
                     line: 3,
                     column: 22,
                 },
-                end: GraphQLLocation {
+                end: LineColumn {
                     line: 3,
                     column: 23,
                 }
@@ -278,8 +278,8 @@ fn validation_without_type_system() {
         assert_eq!(
             diag.line_column_range(),
             Some(Range {
-                start: GraphQLLocation { line: 1, column: 3 },
-                end: GraphQLLocation { line: 1, column: 7 },
+                start: LineColumn { line: 1, column: 3 },
+                end: LineColumn { line: 1, column: 7 },
             })
         );
         json.assert_eq(&serde_json::to_string_pretty(&diag.to_json()).unwrap());