Repository: apollographql/apollo-rs
Pull request #923 (2024-10-29 15:54:02)

Validate against reserved names starting with `__` in schemas
Fixes https://github.com/apollographql/apollo-rs/issues/743


Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index 5f56483..4c6d6d8 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -17,6 +17,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 ## Maintenance
 ## Documentation-->
 
+# [x.x.x] (unreleased) - 2024-mm-dd
+
+## Fixes
+- **Validate against reserved names starting with `__` in schemas - [SimonSapin], [pull/923].**
+
+[SimonSapin]: https://github.com/SimonSapin
+[pull/923]: https://github.com/apollographql/apollo-rs/issues/923
+
+
 # [1.0.0-beta.24](https://crates.io/crates/apollo-compiler/1.0.0-beta.24) - 2024-09-24
 
 ## BREAKING
diff --git a/crates/apollo-compiler/src/schema/validation.rs b/crates/apollo-compiler/src/schema/validation.rs
index 5c3ec88..ed175bf 100644
--- a/crates/apollo-compiler/src/schema/validation.rs
+++ b/crates/apollo-compiler/src/schema/validation.rs
@@ -1,7 +1,9 @@
 use super::ExtendedType;
 use crate::collections::HashMap;
 use crate::collections::HashSet;
+use crate::parser::FileId;
 use crate::schema::ScalarType;
+use crate::validation::diagnostics::DiagnosticData;
 use crate::validation::directive::validate_directive_definitions;
 use crate::validation::enum_::validate_enum_definition;
 use crate::validation::input_object::validate_input_object_definition;
@@ -20,7 +22,8 @@ pub(crate) fn validate_schema(errors: &mut DiagnosticList, schema: &mut Schema)
     let mut builtin_scalars = BuiltInScalars::new();
     validate_schema_definition(errors, schema);
     validate_directive_definitions(errors, schema, &mut builtin_scalars);
-    for def in schema.types.values() {
+    for (name, def) in &schema.types {
+        validate_type_system_name(errors, name, def.describe());
         match def {
             ExtendedType::Scalar(def) => validate_scalar_definition(errors, schema, def),
             ExtendedType::Object(def) => {
@@ -67,6 +70,25 @@ pub(crate) fn validate_schema(errors: &mut DiagnosticList, schema: &mut Schema)
     }
 }
 
+/// <https://spec.graphql.org/draft/#sec-Names.Reserved-Names>
+pub(crate) fn validate_type_system_name(
+    errors: &mut DiagnosticList,
+    name: &Name,
+    describe: &'static str,
+) {
+    let location = name.location();
+    let is_built_in = location.is_some_and(|loc| loc.file_id == FileId::BUILT_IN);
+    if !is_built_in && name.starts_with("__") {
+        errors.push(
+            location,
+            DiagnosticData::ReservedName {
+                name: name.clone(),
+                describe,
+            },
+        );
+    }
+}
+
 /// Keeps track of usage of [built-in scalars] in a schema to determine which definitions
 /// should be removed or added.
 ///
diff --git a/crates/apollo-compiler/src/validation/diagnostics.rs b/crates/apollo-compiler/src/validation/diagnostics.rs
index ab74ac2..26d6b7a 100644
--- a/crates/apollo-compiler/src/validation/diagnostics.rs
+++ b/crates/apollo-compiler/src/validation/diagnostics.rs
@@ -288,6 +288,10 @@ pub(crate) enum DiagnosticData {
         type_location: Option<SourceSpan>,
         extensions_locations: Vec<Option<SourceSpan>>,
     },
+    #[error(
+        "{describe} cannot be named `{name}` as names starting with two underscores are reserved"
+    )]
+    ReservedName { name: Name, describe: &'static str },
 }
 
 impl DiagnosticData {
@@ -714,6 +718,9 @@ impl DiagnosticData {
                     "input values",
                 );
             }
+            DiagnosticData::ReservedName { name, .. } => {
+                report.with_label_opt(name.location(), "Pick a different name here");
+            }
         }
     }
 
diff --git a/crates/apollo-compiler/src/validation/directive.rs b/crates/apollo-compiler/src/validation/directive.rs
index 20a17df..931ba16 100644
--- a/crates/apollo-compiler/src/validation/directive.rs
+++ b/crates/apollo-compiler/src/validation/directive.rs
@@ -144,6 +144,7 @@ pub(crate) fn validate_directive_definition(
     built_in_scalars: &mut BuiltInScalars,
     def: &Node<ast::DirectiveDefinition>,
 ) {
+    schema::validation::validate_type_system_name(diagnostics, &def.name, "a directive definition");
     super::input_object::validate_argument_definitions(
         diagnostics,
         schema,
diff --git a/crates/apollo-compiler/src/validation/enum_.rs b/crates/apollo-compiler/src/validation/enum_.rs
index 9c7ec34..f207485 100644
--- a/crates/apollo-compiler/src/validation/enum_.rs
+++ b/crates/apollo-compiler/src/validation/enum_.rs
@@ -45,6 +45,11 @@ pub(crate) fn validate_enum_value(
     schema: &crate::Schema,
     enum_val: &Node<ast::EnumValueDefinition>,
 ) {
+    crate::schema::validation::validate_type_system_name(
+        diagnostics,
+        &enum_val.value,
+        "an enum value",
+    );
     super::directive::validate_directives(
         diagnostics,
         Some(schema),
diff --git a/crates/apollo-compiler/src/validation/field.rs b/crates/apollo-compiler/src/validation/field.rs
index 35cf20d..9f9324c 100644
--- a/crates/apollo-compiler/src/validation/field.rs
+++ b/crates/apollo-compiler/src/validation/field.rs
@@ -137,6 +137,7 @@ pub(crate) fn validate_field_definition(
     built_in_scalars: &mut BuiltInScalars,
     field: &Node<ast::FieldDefinition>,
 ) {
+    crate::schema::validation::validate_type_system_name(diagnostics, &field.name, "a field");
     super::directive::validate_directives(
         diagnostics,
         Some(schema),
diff --git a/crates/apollo-compiler/src/validation/input_object.rs b/crates/apollo-compiler/src/validation/input_object.rs
index fe94e29..77c796f 100644
--- a/crates/apollo-compiler/src/validation/input_object.rs
+++ b/crates/apollo-compiler/src/validation/input_object.rs
@@ -114,6 +114,7 @@ pub(crate) fn validate_input_object_definition(
         built_in_scalars,
         &fields,
         ast::DirectiveLocation::InputFieldDefinition,
+        "an input object field",
     );
 
     // validate there is at least one input value on the input object type
@@ -147,6 +148,7 @@ pub(crate) fn validate_argument_definitions(
         built_in_scalars,
         input_values,
         directive_location,
+        "an argument",
     );
 
     let mut seen: HashMap<Name, &Node<ast::InputValueDefinition>> = HashMap::default();
@@ -176,8 +178,14 @@ pub(crate) fn validate_input_value_definitions(
     built_in_scalars: &mut BuiltInScalars,
     input_values: &[Node<ast::InputValueDefinition>],
     directive_location: ast::DirectiveLocation,
+    describe: &'static str,
 ) {
     for input_value in input_values {
+        crate::schema::validation::validate_type_system_name(
+            diagnostics,
+            &input_value.name,
+            describe,
+        );
         super::directive::validate_directives(
             diagnostics,
             Some(schema),
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index 920752a..3264cd8 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -331,6 +331,7 @@ impl DiagnosticData {
                     EmptyValueSet { .. } => "EmptyValueSet",
                     EmptyMemberSet { .. } => "EmptyMemberSet",
                     EmptyInputValueSet { .. } => "EmptyInputValueSet",
+                    ReservedName { .. } => "ReservedName",
                 })
             }
             Details::ExecutableBuildError(error) => Some(match error {
@@ -527,6 +528,7 @@ impl DiagnosticData {
                     EmptyValueSet { .. } => None,
                     EmptyMemberSet { .. } => None,
                     EmptyInputValueSet { .. } => None,
+                    ReservedName { .. } => None,
                 }
             }
             Details::ExecutableBuildError(error) => match error {


Test patch:
diff --git a/crates/apollo-compiler/test_data/diagnostics/0051_subscription_operation_with_root_introspection_field.txt b/crates/apollo-compiler/test_data/diagnostics/0051_subscription_operation_with_root_introspection_field.txt
index 6c69550..3105e79 100644
--- a/crates/apollo-compiler/test_data/diagnostics/0051_subscription_operation_with_root_introspection_field.txt
+++ b/crates/apollo-compiler/test_data/diagnostics/0051_subscription_operation_with_root_introspection_field.txt
@@ -5,4 +5,11 @@ Error: subscription `sub` can not have an introspection field as a root field
    │   ─────┬────  
    │        ╰────── __typename is an introspection field
 ───╯
+Error: a field cannot be named `__typename` as names starting with two underscores are reserved
+   ╭─[0051_subscription_operation_with_root_introspection_field.graphql:6:3]
+   │
+ 6 │   __typename: String
+   │   ─────┬────  
+   │        ╰────── Pick a different name here
+───╯
 
diff --git a/crates/apollo-compiler/test_data/diagnostics/0119_reserved_names.graphql b/crates/apollo-compiler/test_data/diagnostics/0119_reserved_names.graphql
new file mode 100644
index 0000000..85d24ad
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0119_reserved_names.graphql
@@ -0,0 +1,21 @@
+directive @__Prime(__if: Boolean!) on SCHEMA
+
+schema @__Prime(__if: true) {
+    query: __MyQuery
+}
+
+type __MyQuery {
+    __secretField(__heatedArgument: __In): Int
+}
+
+input __In {
+    __amount: __BigInt
+}
+
+scalar __BigInt
+
+enum __Maybe {
+    Yes
+    No
+    __FileNotFound
+}
diff --git a/crates/apollo-compiler/test_data/diagnostics/0119_reserved_names.txt b/crates/apollo-compiler/test_data/diagnostics/0119_reserved_names.txt
new file mode 100644
index 0000000..e652352
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0119_reserved_names.txt
@@ -0,0 +1,71 @@
+Error: a directive definition cannot be named `__Prime` as names starting with two underscores are reserved
+   ╭─[0119_reserved_names.graphql:1:12]
+   │
+ 1 │ directive @__Prime(__if: Boolean!) on SCHEMA
+   │            ───┬───  
+   │               ╰───── Pick a different name here
+───╯
+Error: an argument cannot be named `__if` as names starting with two underscores are reserved
+   ╭─[0119_reserved_names.graphql:1:20]
+   │
+ 1 │ directive @__Prime(__if: Boolean!) on SCHEMA
+   │                    ──┬─  
+   │                      ╰─── Pick a different name here
+───╯
+Error: an object type cannot be named `__MyQuery` as names starting with two underscores are reserved
+   ╭─[0119_reserved_names.graphql:7:6]
+   │
+ 7 │ type __MyQuery {
+   │      ────┬────  
+   │          ╰────── Pick a different name here
+───╯
+Error: a field cannot be named `__secretField` as names starting with two underscores are reserved
+   ╭─[0119_reserved_names.graphql:8:5]
+   │
+ 8 │     __secretField(__heatedArgument: __In): Int
+   │     ──────┬──────  
+   │           ╰──────── Pick a different name here
+───╯
+Error: an argument cannot be named `__heatedArgument` as names starting with two underscores are reserved
+   ╭─[0119_reserved_names.graphql:8:19]
+   │
+ 8 │     __secretField(__heatedArgument: __In): Int
+   │                   ────────┬───────  
+   │                           ╰───────── Pick a different name here
+───╯
+Error: an input object type cannot be named `__In` as names starting with two underscores are reserved
+    ╭─[0119_reserved_names.graphql:11:7]
+    │
+ 11 │ input __In {
+    │       ──┬─  
+    │         ╰─── Pick a different name here
+────╯
+Error: an input object field cannot be named `__amount` as names starting with two underscores are reserved
+    ╭─[0119_reserved_names.graphql:12:5]
+    │
+ 12 │     __amount: __BigInt
+    │     ────┬───  
+    │         ╰───── Pick a different name here
+────╯
+Error: a scalar type cannot be named `__BigInt` as names starting with two underscores are reserved
+    ╭─[0119_reserved_names.graphql:15:8]
+    │
+ 15 │ scalar __BigInt
+    │        ────┬───  
+    │            ╰───── Pick a different name here
+────╯
+Error: an enum type cannot be named `__Maybe` as names starting with two underscores are reserved
+    ╭─[0119_reserved_names.graphql:17:6]
+    │
+ 17 │ enum __Maybe {
+    │      ───┬───  
+    │         ╰───── Pick a different name here
+────╯
+Error: an enum value cannot be named `__FileNotFound` as names starting with two underscores are reserved
+    ╭─[0119_reserved_names.graphql:20:5]
+    │
+ 20 │     __FileNotFound
+    │     ───────┬──────  
+    │            ╰──────── Pick a different name here
+────╯
+
diff --git a/crates/apollo-compiler/test_data/serializer/diagnostics/0119_reserved_named.graphql b/crates/apollo-compiler/test_data/serializer/diagnostics/0119_reserved_named.graphql
new file mode 100644
index 0000000..8ca6e4f
--- /dev/null
+++ b/crates/apollo-compiler/test_data/serializer/diagnostics/0119_reserved_named.graphql
@@ -0,0 +1,20 @@
+directive @__Prime on SCHEMA
+
+schema @__Prime {
+  query: __MyQuery
+}
+
+type __MyQuery {
+  __secretField(__heatedArgument: __In): Int
+}
+
+input __In {
+  __amount: __BigInt
+}
+
+scalar __BigInt
+
+enum __Maybe {
+  __Yes
+  __No
+}
diff --git a/crates/apollo-compiler/test_data/serializer/diagnostics/0119_reserved_names.graphql b/crates/apollo-compiler/test_data/serializer/diagnostics/0119_reserved_names.graphql
new file mode 100644
index 0000000..3768c70
--- /dev/null
+++ b/crates/apollo-compiler/test_data/serializer/diagnostics/0119_reserved_names.graphql
@@ -0,0 +1,21 @@
+directive @__Prime(__if: Boolean!) on SCHEMA
+
+schema @__Prime(__if: true) {
+  query: __MyQuery
+}
+
+type __MyQuery {
+  __secretField(__heatedArgument: __In): Int
+}
+
+input __In {
+  __amount: __BigInt
+}
+
+scalar __BigInt
+
+enum __Maybe {
+  Yes
+  No
+  __FileNotFound
+}