Skip to content

Commit c85259e

Browse files
captbaritonemeta-codesync[bot]
authored andcommitted
Gate @RelayResolver usage behind allow_legacy_relay_resolver_tag in relay-schema-generation
Reviewed By: evanyeung Differential Revision: D94138342 fbshipit-source-id: 38f196089c191e1e587d0b69b00c988c2d0f5007
1 parent 1afa309 commit c85259e

22 files changed

Lines changed: 553 additions & 28 deletions

compiler/crates/docblock-shared/src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@ use intern::string_key::StringKey;
1515
use lazy_static::lazy_static;
1616
pub use resolver_source_hash::ResolverSourceHash;
1717

18+
/// Check if a string contains any resolver docblock tag
19+
/// (`@RelayResolver`, `@relayType`, or `@relayField`).
20+
///
21+
/// This performs a single pass over the string, avoiding the cost of
22+
/// multiple `str::contains` calls which would each iterate the full text.
23+
pub fn contains_resolver_tag(text: &str) -> bool {
24+
let bytes = text.as_bytes();
25+
let len = bytes.len();
26+
let mut i = 0;
27+
while i < len {
28+
if bytes[i] == b'@' {
29+
let remaining = &text[i + 1..];
30+
if remaining.starts_with("RelayResolver")
31+
|| remaining.starts_with("relayType")
32+
|| remaining.starts_with("relayField")
33+
{
34+
return true;
35+
}
36+
}
37+
i += 1;
38+
}
39+
false
40+
}
41+
1842
lazy_static! {
1943
/// Resolver fields and types get their schema definitions annotated with
2044
/// a directive using this name to signal to the rest of Relay that they are backed by

compiler/crates/extract-graphql/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ fn contains_resolver_tag(text: &str) -> bool {
116116
false
117117
}
118118

119-
/// Extract graphql`text` literals and @RelayResolver comments from JS-like code.
119+
/// Extract graphql`text` literals and Relay Resolver docblock comments from JS-like code.
120120
// This should work for Flow or TypeScript alike.
121121
pub fn extract(input: &str) -> Vec<JavaScriptSourceFeature> {
122122
let mut res = Vec::new();

compiler/crates/relay-compiler/src/build_project/build_resolvers_schema/extract_docblock_ir.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ fn extract_docblock_ir_for_project(
103103
&project_config.typegen_config.custom_scalar_types,
104104
compiler_state,
105105
graphql_asts,
106+
&project_config.feature_flags.allow_legacy_relay_resolver_tag,
106107
)?;
107108
type_irs.extend(extracted_types);
108109
field_irs.extend(extracted_fields);

compiler/crates/relay-compiler/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use std::vec;
1717
use async_trait::async_trait;
1818
use common::DiagnosticsResult;
1919
use common::DirectiveName;
20+
use common::FeatureFlag;
2021
use common::FeatureFlags;
2122
use common::Rollout;
2223
use common::ScalarName;
@@ -108,6 +109,7 @@ type CustomExtractRelayResolvers = Box<
108109
&FnvIndexMap<ScalarName, CustomType>,
109110
&CompilerState,
110111
Option<&GraphQLAsts>,
112+
&FeatureFlag,
111113
) -> DiagnosticsResult<(Vec<DocblockIr>, Vec<DocblockIr>)>
112114
// (Types, Fields)
113115
+ Send

compiler/crates/relay-schema-generation/src/errors.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use common::DiagnosticDisplay;
9+
use common::WithDiagnosticData;
10+
use docblock_shared::RELAY_FIELD_FIELD;
11+
use docblock_shared::RELAY_TYPE_FIELD;
812
use intern::string_key::StringKey;
913
use thiserror::Error;
1014

@@ -38,16 +42,16 @@ pub enum SchemaGenerationError {
3842
ExpectedTypeAliasToBeObject,
3943
#[error("Expected object definition to include fields")]
4044
ExpectedWeakObjectToHaveFields,
41-
#[error("@RelayResolver annotation is expected to be on a named export")]
45+
#[error("Relay Resolver annotation is expected to be on a named export")]
4246
ExpectedNamedExport,
43-
#[error("@RelayResolver annotation is expected to be on a named function or type alias")]
47+
#[error("Relay Resolver annotation is expected to be on a named function or type alias")]
4448
ExpectedFunctionOrTypeAlias,
4549
#[error(
46-
"Types used in @RelayResolver definitions should be imported using named or default imports (without using a `*`)"
50+
"Types used in Relay Resolver definitions should be imported using named or default imports (without using a `*`)"
4751
)]
4852
UseNamedOrDefaultImport,
4953
#[error(
50-
"Failed to find @RelayResolver type definition for `{entity_name}` using a {export_type} import from module `{module_name}`. Please make sure `{entity_name}` is either defined locally or imported using a named or default import and that it is a resolver type"
54+
"Failed to find Relay Resolver type definition for `{entity_name}` using a {export_type} import from module `{module_name}`. Please make sure `{entity_name}` is either defined locally or imported using a named or default import and that it is a resolver type"
5155
)]
5256
ModuleNotFound {
5357
entity_name: StringKey,
@@ -72,7 +76,7 @@ pub enum SchemaGenerationError {
7276
)]
7377
IncorrectArgumentsDefinition,
7478
#[error(
75-
"Multiple docblock descriptions found for this @RelayResolver. Please only include one description (a comment in the docblock uninterrupted by a resolver \"@<field>\")"
79+
"Multiple docblock descriptions found for this Relay Resolver. Please only include one description (a comment in the docblock uninterrupted by a resolver \"@<field>\")"
7680
)]
7781
MultipleDocblockDescriptions,
7882
#[error(
@@ -107,3 +111,45 @@ pub enum SchemaGenerationError {
107111
)]
108112
ExpectedResolverFunctionWithRootFragment,
109113
}
114+
115+
#[derive(
116+
Clone,
117+
Debug,
118+
Error,
119+
Eq,
120+
PartialEq,
121+
Ord,
122+
PartialOrd,
123+
Hash,
124+
serde::Serialize
125+
)]
126+
#[serde(tag = "type")]
127+
pub enum SchemaGenerationErrorWithData {
128+
#[error(
129+
"Unexpected `@RelayResolver` for a type definition. Expected `@relayType`. The legacy `@RelayResolver` tag can be enabled with the `allow_legacy_relay_resolver_tag` feature flag."
130+
)]
131+
UseRelayTypeTag,
132+
#[error(
133+
"Unexpected `@RelayResolver` for a field definition. Expected `@relayField`. The legacy `@RelayResolver` tag can be enabled with the `allow_legacy_relay_resolver_tag` feature flag."
134+
)]
135+
UseRelayFieldTag,
136+
#[error("Unexpected `@relayType` for a field definition. Expected `@relayField`.")]
137+
RelayTypeTagUsedForField,
138+
#[error("Unexpected `@relayField` for a type definition. Expected `@relayType`.")]
139+
RelayFieldTagUsedForType,
140+
}
141+
142+
impl WithDiagnosticData for SchemaGenerationErrorWithData {
143+
fn get_data(&self) -> Vec<Box<dyn DiagnosticDisplay>> {
144+
match self {
145+
SchemaGenerationErrorWithData::UseRelayTypeTag => vec![Box::new(*RELAY_TYPE_FIELD)],
146+
SchemaGenerationErrorWithData::UseRelayFieldTag => vec![Box::new(*RELAY_FIELD_FIELD)],
147+
SchemaGenerationErrorWithData::RelayTypeTagUsedForField => {
148+
vec![Box::new(*RELAY_FIELD_FIELD)]
149+
}
150+
SchemaGenerationErrorWithData::RelayFieldTagUsedForType => {
151+
vec![Box::new(*RELAY_TYPE_FIELD)]
152+
}
153+
}
154+
}
155+
}

compiler/crates/relay-schema-generation/src/lib.rs

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,20 @@ use ::intern::string_key::Intern;
2626
use ::intern::string_key::StringKey;
2727
use common::Diagnostic;
2828
use common::DiagnosticsResult;
29+
use common::FeatureFlag;
2930
use common::Location;
3031
use common::ScalarName;
3132
use common::SourceLocationKey;
3233
use common::Span;
3334
use common::WithLocation;
3435
use docblock_shared::DEPRECATED_FIELD;
3536
use docblock_shared::ResolverSourceHash;
37+
use docblock_shared::contains_resolver_tag;
3638
use docblock_syntax::DocblockAST;
3739
use docblock_syntax::DocblockSection;
3840
use docblock_syntax::parse_docblock_with_offset;
3941
use errors::SchemaGenerationError;
42+
use errors::SchemaGenerationErrorWithData;
4043
use find_resolver_imports::ImportExportVisitor;
4144
use find_resolver_imports::JSImportType;
4245
use find_resolver_imports::ModuleResolution;
@@ -138,6 +141,10 @@ pub struct RelayResolverExtractor {
138141

139142
// Used to map Flow types in return/argument types to GraphQL custom scalars
140143
custom_scalar_map: FnvIndexMap<CustomType, ScalarName>,
144+
145+
// Feature flag controlling whether the legacy @RelayResolver tag is allowed
146+
// in place of @relayType / @relayField
147+
allow_legacy_relay_resolver_tag: FeatureFlag,
141148
}
142149

143150
enum FieldDefinitionInfo {
@@ -152,6 +159,12 @@ enum FieldDefinitionInfo {
152159
},
153160
}
154161

162+
enum UsedTag {
163+
LegacyResolver,
164+
Type,
165+
Field,
166+
}
167+
155168
struct UnresolvedFieldDefinition {
156169
entity_name: Option<WithLocation<StringKey>>,
157170
field_name: WithLocation<StringKey>,
@@ -163,21 +176,16 @@ struct UnresolvedFieldDefinition {
163176
field_info: FieldDefinitionInfo,
164177
}
165178

166-
impl Default for RelayResolverExtractor {
167-
fn default() -> Self {
168-
Self::new()
169-
}
170-
}
171-
172179
impl RelayResolverExtractor {
173-
pub fn new() -> Self {
180+
pub fn new(allow_legacy_relay_resolver_tag: &FeatureFlag) -> Self {
174181
let mut self_ = Self {
175182
type_definitions: Default::default(),
176183
unresolved_field_definitions: Default::default(),
177184
resolved_field_definitions: vec![],
178185
module_resolutions: Default::default(),
179186
current_location: SourceLocationKey::generated(),
180187
custom_scalar_map: FnvIndexMap::default(),
188+
allow_legacy_relay_resolver_tag: allow_legacy_relay_resolver_tag.clone(),
181189
};
182190
self_.add_relay_runtime_flow_scalars();
183191
self_
@@ -209,7 +217,8 @@ impl RelayResolverExtractor {
209217
source_module_path: &str,
210218
fragment_definitions: Option<&Vec<ExecutableDefinition>>,
211219
) -> DiagnosticsResult<()> {
212-
// Assume the caller knows the text contains at least one RelayResolver decorator
220+
// Assume the caller knows the text contains at least one resolver docblock tag
221+
// (@relayType, @relayField, or the legacy @RelayResolver)
213222

214223
self.current_location = SourceLocationKey::standalone(source_module_path);
215224

@@ -262,7 +271,7 @@ impl RelayResolverExtractor {
262271
let result = try_all(
263272
attached_comments
264273
.into_iter()
265-
.filter(|(comment, _, _, _)| comment.contains("@RelayResolver"))
274+
.filter(|(comment, _, _, _)| contains_resolver_tag(comment))
266275
.map(|(comment, comment_range, node, range)| {
267276
// TODO: Handle unwraps
268277
// Hermes strips the /* and */ delimiters from the
@@ -274,7 +283,16 @@ impl RelayResolverExtractor {
274283
self.current_location,
275284
comment_range.start + 2,
276285
)?;
277-
let resolver_value = docblock.find_field(intern!("RelayResolver")).unwrap();
286+
let (used_tag, resolver_value) =
287+
if let Some(field) = docblock.find_field(intern!("RelayResolver")) {
288+
(UsedTag::LegacyResolver, field)
289+
} else if let Some(field) = docblock.find_field(intern!("relayType")) {
290+
(UsedTag::Type, field)
291+
} else if let Some(field) = docblock.find_field(intern!("relayField")) {
292+
(UsedTag::Field, field)
293+
} else {
294+
return Ok(());
295+
};
278296

279297
let deprecated = get_deprecated(&docblock);
280298
let description = get_description(&docblock, comment_range)?;
@@ -289,15 +307,50 @@ impl RelayResolverExtractor {
289307
}) => {
290308
let name = resolver_value.field_value.unwrap_or(field_name);
291309

292-
// Heuristic to treat lowercase name as field definition, otherwise object definition
293-
// if there is a `.` in the name, it is the old resolver synatx, e.g. @RelayResolver Client.field,
294-
// we should treat it as a field definition
310+
// Heuristic to treat lowercase name as field definition, otherwise object definition.
311+
// If there is a `.` in the name, it is the old verbose syntax,
312+
// e.g. @relayField Client.field; we should treat it as a field definition
295313
let is_field_definition = {
296314
let name_str = name.item.lookup();
297315
let is_lowercase_initial =
298316
name_str.chars().next().unwrap().is_lowercase();
299317
is_lowercase_initial || name_str.contains('.')
300318
};
319+
320+
match used_tag {
321+
UsedTag::LegacyResolver => {
322+
if !self
323+
.allow_legacy_relay_resolver_tag
324+
.is_enabled_for(name.item)
325+
{
326+
return Err(vec![Diagnostic::error_with_data(
327+
if is_field_definition {
328+
SchemaGenerationErrorWithData::UseRelayFieldTag
329+
} else {
330+
SchemaGenerationErrorWithData::UseRelayTypeTag
331+
},
332+
resolver_value.field_name.location,
333+
)]);
334+
}
335+
}
336+
UsedTag::Type => {
337+
if is_field_definition {
338+
return Err(vec![Diagnostic::error_with_data(
339+
SchemaGenerationErrorWithData::RelayTypeTagUsedForField,
340+
resolver_value.field_name.location,
341+
)]);
342+
}
343+
}
344+
UsedTag::Field => {
345+
if !is_field_definition {
346+
return Err(vec![Diagnostic::error_with_data(
347+
SchemaGenerationErrorWithData::RelayFieldTagUsedForType,
348+
resolver_value.field_name.location,
349+
)]);
350+
}
351+
}
352+
}
353+
301354
if is_field_definition {
302355
let entity_name = match entity_type {
303356
Some(entity_type) => {
@@ -340,6 +393,30 @@ impl RelayResolverExtractor {
340393
type_alias,
341394
}) => {
342395
let name = resolver_value.field_value.unwrap_or(field_name);
396+
397+
match used_tag {
398+
UsedTag::LegacyResolver => {
399+
if !self
400+
.allow_legacy_relay_resolver_tag
401+
.is_enabled_for(name.item)
402+
{
403+
return Err(vec![Diagnostic::error_with_data(
404+
SchemaGenerationErrorWithData::UseRelayTypeTag,
405+
resolver_value.field_name.location,
406+
)]);
407+
}
408+
}
409+
UsedTag::Field => {
410+
return Err(vec![Diagnostic::error_with_data(
411+
SchemaGenerationErrorWithData::RelayFieldTagUsedForType,
412+
resolver_value.field_name.location,
413+
)]);
414+
}
415+
UsedTag::Type => {
416+
// This is the expected tag! No errors to report.
417+
}
418+
}
419+
343420
let mut prop_visitor = PropertyVisitor::new(
344421
source_module_path,
345422
source_hash,

compiler/crates/relay-schema-generation/tests/docblock.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::str::FromStr;
1111
use std::sync::Arc;
1212

1313
use common::Diagnostic;
14+
use common::FeatureFlag;
1415
use common::ScalarName;
1516
use common::SourceLocationKey;
1617
use common::TextSource;
@@ -42,7 +43,7 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result<String, String>
4243
let project_fixture = ProjectFixture::deserialize(fixture.content);
4344

4445
let custom_scalar_types = get_custom_scalar_types();
45-
let mut extractor = RelayResolverExtractor::new();
46+
let mut extractor = RelayResolverExtractor::new(&FeatureFlag::Enabled);
4647
if let Err(err) = extractor.set_custom_scalar_map(&custom_scalar_types) {
4748
errors.extend(err);
4849
}

compiler/crates/relay-schema-generation/tests/docblock/fixtures/description.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ type Cat @__RelayResolverModel {
202202
}
203203

204204

205-
✖︎ Multiple docblock descriptions found for this @RelayResolver. Please only include one description (a comment in the docblock uninterrupted by a resolver "@<field>")
205+
✖︎ Multiple docblock descriptions found for this Relay Resolver. Please only include one description (a comment in the docblock uninterrupted by a resolver "@<field>")
206206

207207
module.js:24:1
208208
22 │ export function name(cat: CatFlowType): ?string {}

compiler/crates/relay-schema-generation/tests/docblock/fixtures/incorrect-export-error.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const MyResolver = {x: number};
1818
==================================== OUTPUT ===================================
1919

2020

21-
✖︎ @RelayResolver annotation is expected to be on a named export
21+
✖︎ Relay Resolver annotation is expected to be on a named export
2222

2323
module.js:7:1
2424
5 │ * @RelayResolver

compiler/crates/relay-schema-generation/tests/docblock/fixtures/incorrect-type-error.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const MyResolver = string;
1818
==================================== OUTPUT ===================================
1919

2020

21-
✖︎ @RelayResolver annotation is expected to be on a named function or type alias
21+
✖︎ Relay Resolver annotation is expected to be on a named function or type alias
2222

2323
module.js:7:1
2424
5 │ * @RelayResolver

0 commit comments

Comments
 (0)