diff --git a/compiler/crates/graphql-ir/src/errors.rs b/compiler/crates/graphql-ir/src/errors.rs index 4cfa7c289a3dd..f86d481efbaa4 100644 --- a/compiler/crates/graphql-ir/src/errors.rs +++ b/compiler/crates/graphql-ir/src/errors.rs @@ -498,6 +498,17 @@ pub enum ValidationMessage { deprecation_reason: Option, }, + #[error("The enum value `{enum_value}` of type `{enum_name}` is deprecated.{}", + match deprecation_reason { + Some(reason) => format!(" Deprecation reason: \"{reason}\""), + None => "".to_string() + })] + DeprecatedEnumValue { + enum_value: StringKey, + enum_name: StringKey, + deprecation_reason: Option, + }, + #[error("Missing required {}: `{}`", if missing_arg_names.len() > 1 { "arguments" } else { "argument" }, missing_arg_names diff --git a/compiler/crates/relay-transforms/src/validations/deprecated_fields.rs b/compiler/crates/relay-transforms/src/validations/deprecated_fields.rs index 1bfd717c84d02..51a5fa4634f47 100644 --- a/compiler/crates/relay-transforms/src/validations/deprecated_fields.rs +++ b/compiler/crates/relay-transforms/src/validations/deprecated_fields.rs @@ -12,6 +12,7 @@ use common::DiagnosticTag; use common::DiagnosticsResult; use common::WithLocation; use graphql_ir::Argument; +use graphql_ir::ConstantValue; use graphql_ir::Directive; use graphql_ir::ExecutableDefinition; use graphql_ir::LinkedField; @@ -24,6 +25,7 @@ use graphql_ir::Value; use schema::FieldID; use schema::SDLSchema; use schema::Schema; +use schema::Type; use crate::fragment_alias_directive::FRAGMENT_DANGEROUSLY_UNALIAS_DIRECTIVE_NAME; @@ -81,22 +83,42 @@ impl<'a> DeprecatedFields<'a> { } for arg in arguments { - if let Some(arg_definition) = field_definition.arguments.named(arg.name.item) - && let Some(directive) = arg_definition.deprecated() - { - let parent_type = field_definition.parent_type.unwrap(); - let parent_name = schema.get_type_name(parent_type); - - self.warnings.push(Diagnostic::hint( - ValidationMessage::DeprecatedFieldArgument { - argument_name: arg.name.item, - field_name: field_definition.name.item, - parent_name, - deprecation_reason: directive.reason, - }, - arg.name.location, - vec![DiagnosticTag::DEPRECATED], - )); + if let Some(arg_definition) = field_definition.arguments.named(arg.name.item) { + if let Some(directive) = arg_definition.deprecated() { + let parent_type = field_definition.parent_type.unwrap(); + let parent_name = schema.get_type_name(parent_type); + + self.warnings.push(Diagnostic::hint( + ValidationMessage::DeprecatedFieldArgument { + argument_name: arg.name.item, + field_name: field_definition.name.item, + parent_name, + deprecation_reason: directive.reason, + }, + arg.name.location, + vec![DiagnosticTag::DEPRECATED], + )); + } + + if let Type::Enum(enum_id) = arg_definition.type_.inner() { + let enum_def = schema.enum_(enum_id); + if let Value::Constant(ConstantValue::Enum(value_name)) = &arg.value.item { + if let Some(enum_value) = + enum_def.values.iter().find(|v| v.value == *value_name) + && let Some(deprecation) = enum_value.deprecated() + { + self.warnings.push(Diagnostic::hint( + ValidationMessage::DeprecatedEnumValue { + enum_value: *value_name, + enum_name: enum_def.name.item.0, + deprecation_reason: deprecation.reason, + }, + arg.value.location, + vec![DiagnosticTag::DEPRECATED], + )); + } + } + } } } } @@ -121,11 +143,9 @@ impl Validator for DeprecatedFields<'_> { } fn validate_value(&mut self, value: &Value) -> DiagnosticsResult<()> { - // TODO: `@deprecated` is allowed on Enum values, so technically we - // should also be validating when someone uses a deprecated enum value - // as an argument, but that will require some additional methods on our - // Schema, and potentially some additional traversal in our validation - // trait to traverse into potentially deep constant objects/arrays. + // TODO: Deprecated enum values inside deeply nested constant objects/arrays + // are not yet validated. Top-level enum values in field and directive arguments + // are handled in validate_field and validate_directive respectively. self.default_validate_value(value) } @@ -143,18 +163,38 @@ impl Validator for DeprecatedFields<'_> { )); } for arg in &directive.arguments { - if let Some(arg_definition) = directive_definition.arguments.named(arg.name.item) - && let Some(deprecation) = arg_definition.deprecated() - { - self.warnings.push(Diagnostic::hint( - ValidationMessage::DeprecatedDirectiveArgument { - argument_name: arg.name.item, - directive_name: directive.name.item, - deprecation_reason: deprecation.reason, - }, - arg.name.location, - vec![DiagnosticTag::DEPRECATED], - )); + if let Some(arg_definition) = directive_definition.arguments.named(arg.name.item) { + if let Some(deprecation) = arg_definition.deprecated() { + self.warnings.push(Diagnostic::hint( + ValidationMessage::DeprecatedDirectiveArgument { + argument_name: arg.name.item, + directive_name: directive.name.item, + deprecation_reason: deprecation.reason, + }, + arg.name.location, + vec![DiagnosticTag::DEPRECATED], + )); + } + + if let Type::Enum(enum_id) = arg_definition.type_.inner() { + let enum_def = self.schema.enum_(enum_id); + if let Value::Constant(ConstantValue::Enum(value_name)) = &arg.value.item { + if let Some(enum_value) = + enum_def.values.iter().find(|v| v.value == *value_name) + && let Some(deprecation) = enum_value.deprecated() + { + self.warnings.push(Diagnostic::hint( + ValidationMessage::DeprecatedEnumValue { + enum_value: *value_name, + enum_name: enum_def.name.item.0, + deprecation_reason: deprecation.reason, + }, + arg.value.location, + vec![DiagnosticTag::DEPRECATED], + )); + } + } + } } } } diff --git a/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value.expected b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value.expected new file mode 100644 index 0000000000000..0e9dea454f14c --- /dev/null +++ b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value.expected @@ -0,0 +1,20 @@ +==================================== INPUT ==================================== +fragment Foo on MyNewType { + some_field(status: LEGACY) +} +%extensions% +enum Status { + ACTIVE + LEGACY @deprecated +} +type MyNewType { + some_field(status: Status): String +} +==================================== OUTPUT =================================== +ℹ The enum value `LEGACY` of type `Status` is deprecated. + + deprecated_enum_value.graphql:2:22 + 1 │ fragment Foo on MyNewType { + 2 │ some_field(status: LEGACY) + │ ^^^^^^ + 3 │ } diff --git a/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value.graphql b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value.graphql new file mode 100644 index 0000000000000..85c5238a0513a --- /dev/null +++ b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value.graphql @@ -0,0 +1,11 @@ +fragment Foo on MyNewType { + some_field(status: LEGACY) +} +%extensions% +enum Status { + ACTIVE + LEGACY @deprecated +} +type MyNewType { + some_field(status: Status): String +} diff --git a/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.expected b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.expected new file mode 100644 index 0000000000000..c419e68a427c3 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.expected @@ -0,0 +1,20 @@ +==================================== INPUT ==================================== +fragment Foo on MyNewType { + some_field(status: LEGACY) +} +%extensions% +enum Status { + ACTIVE + LEGACY @deprecated(reason: "Use ACTIVE instead.") +} +type MyNewType { + some_field(status: Status): String +} +==================================== OUTPUT =================================== +ℹ The enum value `LEGACY` of type `Status` is deprecated. Deprecation reason: "Use ACTIVE instead." + + deprecated_enum_value_with_reason.graphql:2:22 + 1 │ fragment Foo on MyNewType { + 2 │ some_field(status: LEGACY) + │ ^^^^^^ + 3 │ } diff --git a/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.graphql b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.graphql new file mode 100644 index 0000000000000..dc6e4357775c2 --- /dev/null +++ b/compiler/crates/relay-transforms/tests/validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.graphql @@ -0,0 +1,11 @@ +fragment Foo on MyNewType { + some_field(status: LEGACY) +} +%extensions% +enum Status { + ACTIVE + LEGACY @deprecated(reason: "Use ACTIVE instead.") +} +type MyNewType { + some_field(status: Status): String +} diff --git a/compiler/crates/relay-transforms/tests/validate_deprecated_fields_test.rs b/compiler/crates/relay-transforms/tests/validate_deprecated_fields_test.rs index a040f949cff91..94062882946ce 100644 --- a/compiler/crates/relay-transforms/tests/validate_deprecated_fields_test.rs +++ b/compiler/crates/relay-transforms/tests/validate_deprecated_fields_test.rs @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<523e6262355cff63df80d9ea70e0d2ef>> + * @generated SignedSource<<617806504dd54b22a1692ec28a9db160>> */ mod validate_deprecated_fields; @@ -26,6 +26,20 @@ async fn deprecated_directive_arg_with_reason() { test_fixture(transform_fixture, file!(), "deprecated_directive_arg_with_reason.graphql", "validate_deprecated_fields/fixtures/deprecated_directive_arg_with_reason.expected", input, expected).await; } +#[tokio::test] +async fn deprecated_enum_value() { + let input = include_str!("validate_deprecated_fields/fixtures/deprecated_enum_value.graphql"); + let expected = include_str!("validate_deprecated_fields/fixtures/deprecated_enum_value.expected"); + test_fixture(transform_fixture, file!(), "deprecated_enum_value.graphql", "validate_deprecated_fields/fixtures/deprecated_enum_value.expected", input, expected).await; +} + +#[tokio::test] +async fn deprecated_enum_value_with_reason() { + let input = include_str!("validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.graphql"); + let expected = include_str!("validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.expected"); + test_fixture(transform_fixture, file!(), "deprecated_enum_value_with_reason.graphql", "validate_deprecated_fields/fixtures/deprecated_enum_value_with_reason.expected", input, expected).await; +} + #[tokio::test] async fn deprecated_field_arg() { let input = include_str!("validate_deprecated_fields/fixtures/deprecated_field_arg.graphql"); diff --git a/compiler/crates/schema/src/definitions.rs b/compiler/crates/schema/src/definitions.rs index 57b4fcdde63d9..dbe6fe1b243f8 100644 --- a/compiler/crates/schema/src/definitions.rs +++ b/compiler/crates/schema/src/definitions.rs @@ -695,6 +695,19 @@ pub struct EnumValue { pub description: Option, } +impl EnumValue { + pub fn deprecated(&self) -> Option { + self.directives + .named(*DIRECTIVE_DEPRECATED) + .map(|directive| Deprecation { + reason: directive + .arguments + .named(*ARGUMENT_REASON) + .and_then(|reason| reason.value.get_string_literal()), + }) + } +} + #[derive( Clone, Eq,