Skip to main content

apollo_federation/link/
metadata.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3use std::sync::LazyLock;
4
5use apollo_compiler::Name;
6use apollo_compiler::Schema;
7use apollo_compiler::collections::HashMap;
8use apollo_compiler::collections::IndexMap;
9use apollo_compiler::collections::IndexSet;
10use indexmap::map::Entry;
11
12use crate::bail;
13use crate::error::FederationError;
14use crate::error::MultipleFederationErrors;
15use crate::error::SingleFederationError;
16use crate::error::suggestion::did_you_mean;
17use crate::error::suggestion::suggestion_list;
18use crate::link::ElementName;
19use crate::link::Import;
20use crate::link::Link;
21use crate::link::link_spec_definition::CORE_VERSIONS;
22use crate::link::link_spec_definition::LINK_VERSIONS;
23use crate::link::link_spec_definition::LinkSpecDefinition;
24use crate::link::spec::Identity;
25use crate::link::spec::Url;
26use crate::link::spec_registry::SPEC_REGISTRY;
27use crate::schema::FederationSchema;
28use crate::schema::position::TypeDefinitionPosition;
29
30/// Metadata about a type/directive belonging to an @link application.
31#[derive(Clone, Debug)]
32pub struct LinkedElement {
33    /// Metadata about the @link application the type/directive belongs to.
34    pub link: Arc<Link>,
35    /// The name-in-spec of the type/directive. Note that if the type/directive uses a default name
36    /// but its name-in-spec was imported already under a different name (a.k.a. a shadowing
37    /// import) or its name-in-spec is an invalid name, this method will still consider it to belong
38    /// to that @link application, but its name-in-spec will be `None`.
39    pub name_in_spec: Option<Name>,
40}
41
42/// Metadata about all the applications of @link in a schema.
43// PORT_NOTE: Named `CoreFeatures` in the JS codebase, but "core" is outdated terminology. Note that
44//            while the JS version allowed adding/removing @link applications, this version does not
45//            and instead computes all metadata from the whole schema at construction. This is less
46//            efficient, but slightly less bug-prone; if this becomes a hot function in the future,
47//            we should consider splitting this up to similarly allow @link addition/removal. A
48//            result of this change is that `conflictsByAlias` from the JS codebase doesn't need to
49//            be tracked here.
50#[derive(Clone, Debug)]
51pub struct LinksMetadata {
52    /// The [Link] for the link spec (or core spec) for the schema.
53    link_itself: Arc<Link>,
54    /// The [LinkSpecDefinition] for the link spec (or core spec) for the schema.
55    link_spec_definition: &'static LinkSpecDefinition,
56    /// For specs, a map from their names-in-schema to their [Link]s.
57    // PORT_NOTE: Named `byAlias` in the JS codebase, but this was confusing terminology.
58    by_spec_name_in_schema: IndexMap<Arc<str>, Arc<Link>>,
59    /// For specs, a map from their identities to their [Link]s plus another map from imported
60    /// type/directive names-in-spec to names-in-schema.
61    by_identity: IndexMap<Identity, (Arc<Link>, IndexMap<ElementName, ElementName>)>,
62    /// For imported types/directives, this is a map from their names-in-schema to their [Link]s
63    /// plus names-in-spec.
64    // PORT_NOTE: Named `byImportName` in the JS codebase, but this was confusing terminology.
65    by_import_element_name_in_schema: IndexMap<ElementName, (Arc<Link>, ElementName)>,
66}
67
68impl LinksMetadata {
69    /// Create @link metadata from a schema. Specifically it:
70    /// 1. Finds the bootstrapping @link directive and definition (and ensures there's only one).
71    /// 2. Runs link spec validations against @link applications.
72    /// 3. Checks if imports are for known elements for @link applications with known specs.
73    /// 4. Collects metadata about @link applications.
74    ///
75    /// It does not:
76    /// 1. Expand definitions for any linked specs (including the link spec itself).
77    /// 2. Require definitions to be expanded, other than for the bootstrapping @link directive.
78    /// 3. Run non-link spec validations against @link applications with known specs.
79    /// 4. Run validations to ensure no used shadowing imports (this must be done separately, after
80    ///    definitions have been expanded and referencers collected).
81    // PORT_NOTE: As noted in the comment on `LinksMetadata`, this method constructs metadata all at
82    //            once instead of through individual @link adding removing. Unlike the JS codebase,
83    //            this method doesn't expand definitions for known versions of the federation spec.
84    pub fn from_schema(schema: &Schema) -> Result<Option<LinksMetadata>, FederationError> {
85        // This finds "bootstrap" uses of @link/@core regardless of order. By spec, the bootstrap
86        // directive application must be the first application of @link/@core, but this was not
87        // enforced by the JS implementation, so we match it for backward compatibility.
88        let mut bootstrap_directives = schema
89            .schema_definition
90            .directives
91            .iter()
92            .filter(|d| LinkSpecDefinition::is_bootstrap_directive(schema, d));
93        let Some(bootstrap_directive) = bootstrap_directives.next() else {
94            return Ok(None);
95        };
96        // There must be exactly one bootstrap directive.
97        if let Some(extraneous_directive) = bootstrap_directives.next() {
98            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
99                message: format!(
100                    "Cannot link the link/core feature itself in `{}` since it has already been linked in `{}`",
101                    extraneous_directive.serialize().no_indent(),
102                    bootstrap_directive.serialize().no_indent()
103                )
104            }.into());
105        }
106        // Attempt to parse the bootstrap directive while not knowing the link spec version, but
107        // only keep the URL (which tells us the link spec version).
108        let url =
109            Link::from_directive_application_when_link_spec_unknown(bootstrap_directive, schema)?
110                .url;
111        let link_spec_definition = if url.identity == Identity::link_identity() {
112            LINK_VERSIONS.find(&url.version).ok_or_else(|| {
113                SingleFederationError::UnknownLinkVersion {
114                    message: format!(
115                        r#"Schema uses unknown version {} of the {} spec"#,
116                        url.version,
117                        Identity::LINK_NAME,
118                    ),
119                }
120            })
121        } else if url.identity == Identity::core_identity() {
122            CORE_VERSIONS.find(&url.version).ok_or_else(|| {
123                SingleFederationError::UnknownLinkVersion {
124                    message: format!(
125                        r#"Schema uses unknown version {} of the {} spec"#,
126                        url.version,
127                        Identity::CORE_NAME,
128                    ),
129                }
130            })
131        } else {
132            bail!("Unexpectedly found non-link/core URL for bootstrap directive");
133        }?;
134
135        // Now that we know the @link/@core directive's name-in-schema and the `LinkSpecDefinition`,
136        // we can properly parse each @link/@core directive application.
137        let link_directive_name_in_schema = &bootstrap_directive.name;
138        let mut link_itself: Option<_> = None;
139        let mut by_spec_name_in_schema = Default::default();
140        let mut by_identity = Default::default();
141        let mut by_import_element_name_in_schema = Default::default();
142        // For composed elements, merge will generally keep the names-in-schema of spec elements in
143        // subgraphs as a way to minimize conflicts while keeping element names predictable for
144        // user-defined downstream code. However, merge will also sometimes change the spec of
145        // certain spec elements (e.g. of a federation spec directive). The result of this is that
146        // sometimes elements using a default name of one spec may be imported using another spec,
147        // so we need to permit e.g. the cost spec to import "@cost" as "@federation__cost" in the
148        // supergraph schema. This kind of thing is generally fine, provided the old spec's
149        // name-in-schema is no longer in use in the supergraph schema.
150        //
151        // So whenever an import occurs with an element name-in-schema that uses a prefix but
152        // there's no spec in the schema whose name-in-schema is that prefix, we store an entry here
153        // from the yet-unused prefix to the element name-in-schema. This lets us easily lookup
154        // those elements in `self.by_element_name_in_schema` if a later spec's name-in-schema ends
155        // up equaling that prefix later and we need to generate an error message.
156        //
157        // PORT_NOTE: Unlike the JS code, we don't support @link addition/removal, so we only need
158        // to remember the first such conflict (if we supported removals, we'd need to remember all
159        // such conflicts).
160        let mut first_conflict_by_spec_name_in_schema: IndexMap<Arc<str>, ElementName> =
161            Default::default();
162        for directive in schema.schema_definition.directives.iter() {
163            // Ignore non-@link/@core directives.
164            if &directive.name != link_directive_name_in_schema {
165                continue;
166            }
167            // Properly parse the @link/@core directive application into a `Link`.
168            let link = Arc::new(link_spec_definition.link_from_directive(directive, schema)?);
169            // If this is for the bootstrapped link/core spec, record it.
170            if link.url == url && link_itself.replace(link.clone()).is_some() {
171                bail!("Unexpectedly multiple @link applications for the link/core feature");
172            }
173            Self::add_link(
174                link,
175                &mut by_spec_name_in_schema,
176                &mut by_identity,
177                &mut by_import_element_name_in_schema,
178                &mut first_conflict_by_spec_name_in_schema,
179            )?;
180        }
181        let Some(link_itself) = link_itself else {
182            bail!("Unexpectedly no @link applications for the link/core feature");
183        };
184
185        Ok(Some(LinksMetadata {
186            link_itself,
187            link_spec_definition,
188            by_spec_name_in_schema,
189            by_identity,
190            by_import_element_name_in_schema,
191        }))
192    }
193
194    fn add_link(
195        link: Arc<Link>,
196        by_spec_name_in_schema: &mut IndexMap<Arc<str>, Arc<Link>>,
197        by_identity: &mut IndexMap<Identity, (Arc<Link>, IndexMap<ElementName, ElementName>)>,
198        by_import_element_name_in_schema: &mut IndexMap<ElementName, (Arc<Link>, ElementName)>,
199        first_conflict_by_spec_name_in_schema: &mut IndexMap<Arc<str>, ElementName>,
200    ) -> Result<(), FederationError> {
201        let identity = &link.url.identity;
202        // The identity can't already be mapped to another @link/`Link`. (Even when they're
203        // different major versions, they're usually describing the same capabilities but in
204        // incompatible ways, so we don't want to allow the same schema to try to use multiple of
205        // them.)
206        if by_identity.contains_key(identity) {
207            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
208                message: format!(
209                    r#"Cannot link feature "{}" since it has already been linked in the schema."#,
210                    identity,
211                ),
212            }
213            .into());
214        }
215
216        let spec_name_in_schema = link.spec_name_in_schema();
217        // Normally we'd always forbid "__" in spec names-in-schema. However, there are some older
218        // supergraph schemas that link the "tag" and "inaccessible" specs to the names-in-schema
219        // "federation__tag" and "federation__inaccessible". This is due to bugs in older versions
220        // of composition, but is technically fine since these specs have no types and directives
221        // other than the default directive, so they never prefix anything with "__". So we make a
222        // very specific exception here for that case. We may remove this exception in the future,
223        // once support has been dropped for those bugged composition versions.
224        if !(identity == &Identity::tag_identity()
225            && spec_name_in_schema == "federation__tag"
226            && link.imports.is_empty()
227            || identity == &Identity::inaccessible_identity()
228                && spec_name_in_schema == "federation__inaccessible"
229                && link.imports.is_empty())
230        {
231            // Don't allow spec names-in-schema to have "__" in them, as namespace splitting splits
232            // on the earliest "__" (so a namespaced name with a spec name-in-schema containing "__"
233            // would be erroneously split mid-spec-name-in-schema).
234            if spec_name_in_schema.contains("__") {
235                return Err(SingleFederationError::InvalidLinkDirectiveUsage {
236                    message: format!(
237                        r#"Cannot link feature "{}" as "{}" since it contains "__". Please rename to a compliant name via "as"."#,
238                        identity,
239                        spec_name_in_schema,
240                    ),
241                }.into());
242            }
243        }
244        // Don't allow spec names-in-schema to end in "_", as namespace splitting splits on the
245        // earliest "__" (so a namespaced name with a spec name-in-schema ending with "_" would end
246        // up with "___", and be split before the ending "_" instead of after).
247        if spec_name_in_schema.ends_with("_") {
248            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
249                message: format!(
250                    r#"Cannot link feature "{}" as "{}" since it ends in "_". Please rename to a compliant name via "as"."#,
251                    identity,
252                    spec_name_in_schema,
253                ),
254            }.into());
255        }
256        // Ideally here, we wouldn't allow spec names-in-schema to not be valid GraphQL names.
257        // However, enough supergraph schemas use "." and "-" after the first character that we
258        // can't impose that validation now. So instead, we match using a slightly relaxed regex
259        // that allows "." and "-" after the first character. For schemas that have "." or "-", they
260        // won't be able to use namespaced names for their spec schema elements due to GraphQL
261        // validation, but imports will still work. This is why there's a `Name::new_unchecked()`
262        // call in `Link::spec_name_in_schema()`.
263        //
264        // Note the error message below purposely says "not a valid GraphQL name" because we want to
265        // encourage users to actually use GraphQL names and avoid creating more exceptional cases.
266        if !SPEC_NAME_IN_SCHEMA_REGEX.is_match(&spec_name_in_schema) {
267            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
268                message: format!(
269                    r#"Cannot link feature "{}" as "{}" since it is not a valid GraphQL name. Please rename to a compliant name via "as"."#,
270                    identity,
271                    spec_name_in_schema,
272                ),
273            }.into());
274        }
275        // Don't allow spec names-in-schema to conflict with previous imports that use "__" with a
276        // prefix equal to that spec name-in-schema.
277        if let Some(conflict_import_element_name_in_schema) =
278            first_conflict_by_spec_name_in_schema.get(spec_name_in_schema.as_str())
279        {
280            let Some((conflict_link, conflict_import_element_name_in_spec)) =
281                by_import_element_name_in_schema.get(conflict_import_element_name_in_schema)
282            else {
283                bail!("Unexpectedly cannot find link for import");
284            };
285            let conflict_identity = &conflict_link.url.identity;
286            Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
287            let import_error_message = if conflict_import_element_name_in_spec
288                == conflict_import_element_name_in_schema
289            {
290                format!(r#""{}""#, conflict_import_element_name_in_spec)
291            } else {
292                format!(
293                    r#""{}" as "{}""#,
294                    conflict_import_element_name_in_spec, conflict_import_element_name_in_schema
295                )
296            };
297            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
298                message: format!(
299                    r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
300                    import_error_message,
301                    conflict_identity,
302                    identity,
303                ),
304            }.into());
305        }
306        // Don't allow spec names-in-schema to have default directive names that conflict with
307        // previous imports.
308        let conflict_import_element_name_in_schema = ElementName {
309            name: spec_name_in_schema.clone(),
310            is_directive: true,
311        };
312        if let Some((conflict_link, conflict_import_element_name_in_spec)) =
313            by_import_element_name_in_schema.get(&conflict_import_element_name_in_schema)
314        {
315            let conflict_identity = &conflict_link.url.identity;
316            Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
317            let import_error_message = if conflict_import_element_name_in_spec
318                == &conflict_import_element_name_in_schema
319            {
320                format!(r#""{}""#, conflict_import_element_name_in_spec)
321            } else {
322                format!(
323                    r#""{}" as "{}""#,
324                    conflict_import_element_name_in_spec, conflict_import_element_name_in_schema
325                )
326            };
327            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
328                message: format!(
329                    r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
330                    import_error_message,
331                    conflict_identity,
332                    identity
333                ),
334            }.into());
335        }
336        // The spec name-in-schema can't be already mapped to another @link/`Link`.
337        if let Some(existing_link) = by_spec_name_in_schema.get(spec_name_in_schema.as_str()) {
338            let existing_identity = &existing_link.url.identity;
339            Self::check_tag_inaccessible_conflict(existing_identity, identity)?;
340            return Err(SingleFederationError::InvalidLinkDirectiveUsage {
341                message: format!(
342                    r#"Cannot link feature {} as "{}" since another feature "{}" already uses that alias. Please rename the feature to avoid conflicts via "as"."#,
343                    identity,
344                    spec_name_in_schema,
345                    existing_identity
346                ),
347            }.into());
348        }
349
350        let mut by_identity_imports: IndexMap<ElementName, ElementName> = Default::default();
351        let known_element_names_in_spec: Option<IndexSet<ElementName>> = SPEC_REGISTRY
352            .get_definition(&link.url)
353            .map(|spec_definition| spec_definition.all_element_names().collect());
354        for import in &link.imports {
355            let import_element_name_in_spec = import.element_name_in_spec();
356            let import_element_name_in_schema = import.element_name_in_schema();
357            let import_error_message =
358                if import_element_name_in_spec == import_element_name_in_schema {
359                    format!(r#""{}""#, import_element_name_in_spec)
360                } else {
361                    format!(
362                        r#""{}" as "{}""#,
363                        import_element_name_in_spec, import_element_name_in_schema
364                    )
365                };
366
367            // If the spec is known, the name-in-spec must be known.
368            Self::validate_known_element_name_in_spec(
369                &import_element_name_in_spec,
370                &known_element_names_in_spec,
371            )?;
372            // Only allow mapping to a name with "__" if it's a no-op import or if the prefix isn't
373            // equal to some existing spec's name-in-schema.
374            if let Some((split_spec_name_in_schema, split_name_in_spec)) =
375                Self::split_prefixed_name(&import_element_name_in_schema.name)
376            {
377                if split_spec_name_in_schema == spec_name_in_schema.as_str() {
378                    if split_name_in_spec != import_element_name_in_spec.name.as_str() {
379                        let split_element_name_in_spec = ElementName {
380                            // Only used for error messages, so should be fine.
381                            name: Name::new_unchecked(split_name_in_spec),
382                            is_directive: import.is_directive,
383                        };
384                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
385                            message: format!(
386                                r#"Cannot import {} from feature "{}" since it can be confused with the namespaced name for "{}". Please rename the import to avoid conflicts via "as"."#,
387                                import_error_message,
388                                identity,
389                                split_element_name_in_spec
390                            ),
391                        }.into());
392                    }
393                } else {
394                    if let Some(conflict_link) =
395                        by_spec_name_in_schema.get(split_spec_name_in_schema)
396                    {
397                        let conflict_identity = &conflict_link.url.identity;
398                        Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
399                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
400                            message: format!(
401                                r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
402                                import_error_message,
403                                identity,
404                                conflict_identity
405                            ),
406                        }.into());
407                    } else {
408                        // As mentioned in the comment for `first_conflict_by_spec_name_in_schema`
409                        // in the caller, we have to record the import in case a link gets added
410                        // with the spec name-in-schema later.
411                        first_conflict_by_spec_name_in_schema.insert(
412                            split_spec_name_in_schema.into(),
413                            import_element_name_in_schema.clone(),
414                        );
415                    }
416                }
417            }
418            // For default directives, only allow mapping to a spec name-in-schema if it's a no-op
419            // import.
420            if import.is_directive {
421                if import_element_name_in_schema.name == spec_name_in_schema {
422                    if import_element_name_in_spec.name.as_str() != link.url.identity.name.as_ref()
423                    {
424                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
425                            message: format!(
426                                r#"Cannot import {} from feature "{}" since it can be confused with the namespaced name for "@{}". Please rename the import to avoid conflicts via "as"."#,
427                                import_error_message,
428                                identity,
429                                link.url.identity.name
430                            ),
431                        }.into());
432                    }
433                } else {
434                    if let Some(conflict_link) =
435                        by_spec_name_in_schema.get(import_element_name_in_schema.name.as_str())
436                    {
437                        let conflict_identity = &conflict_link.url.identity;
438                        Self::check_tag_inaccessible_conflict(conflict_identity, identity)?;
439                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
440                            message: format!(
441                                r#"Cannot import {} from feature "{}" since it can be confused with a namespaced name from another linked feature "{}". Please rename the import or feature to avoid conflicts via "as"."#,
442                                import_error_message,
443                                identity,
444                                conflict_identity
445                            ),
446                        }.into());
447                    }
448                }
449            }
450            // The name-in-spec can't be already mapped to a different name-in-schema.
451            match by_identity_imports.entry(import_element_name_in_spec.clone()) {
452                Entry::Occupied(entry) => {
453                    let existing_element_name_in_schema = entry.get();
454                    if existing_element_name_in_schema != &import_element_name_in_schema {
455                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
456                            message: format!(
457                                r#"Cannot import {} from feature "{}" since it was previously imported as "{}". Please remove one of these imports."#,
458                                import_error_message,
459                                identity,
460                                existing_element_name_in_schema
461                            ),
462                        }.into());
463                    }
464                }
465                Entry::Vacant(entry) => {
466                    entry.insert(import_element_name_in_schema.clone());
467                }
468            }
469            // The name-in-schema can't already be mapped to a different name-in-spec.
470            match by_import_element_name_in_schema.entry(import_element_name_in_schema) {
471                Entry::Occupied(entry) => {
472                    let (existing_link, existing_element_name_in_spec) = entry.get();
473                    let existing_identity = &existing_link.url.identity;
474                    if existing_identity != identity {
475                        Self::check_tag_inaccessible_conflict(existing_identity, identity)?;
476                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
477                            message: format!(
478                                r#"Cannot import {} from feature "{}" since it was previously imported from feature "{}". Please rename the import to avoid conflicts via "as"."#,
479                                import_error_message,
480                                identity,
481                                existing_identity
482                            ),
483                        }.into());
484                    }
485                    if existing_element_name_in_spec != &import_element_name_in_spec {
486                        return Err(SingleFederationError::InvalidLinkDirectiveUsage {
487                            message: format!(
488                                r#"Cannot import {} from feature "{}" since it was previously imported for "{}". Please rename the import to avoid conflicts via "as"."#,
489                                import_error_message,
490                                identity,
491                                existing_element_name_in_spec
492                            ),
493                        }.into());
494                    }
495                }
496                Entry::Vacant(entry) => {
497                    entry.insert((link.clone(), import_element_name_in_spec));
498                }
499            }
500        }
501        by_spec_name_in_schema.insert(spec_name_in_schema.into(), link.clone());
502        by_identity.insert(identity.clone(), (link, by_identity_imports));
503        Ok(())
504    }
505
506    /// Returns whether a spec's name-in-schema would pass the checks in `add_link()`, except import
507    /// conflicts are taken from the given map, which should be computed via
508    /// `[Self::compute_spec_name_in_schema_conflicts].
509    pub(crate) fn is_spec_name_in_schema_valid(
510        &self,
511        spec_name_in_schema: &str,
512        identity: &Identity,
513        import_conflicts_by_identity: &ImportConflictsByIdentity,
514    ) -> bool {
515        // Don't allow spec names-in-schema to have "__" in them. Note that this method is only used
516        // in merging to detect whether we need to rename the spec, so we don't need the exception
517        // for "federation__tag" and "federation__inaccessible" here.
518        if spec_name_in_schema.contains("__") {
519            return false;
520        }
521        // Don't allow spec names-in-schema to end in "_".
522        if spec_name_in_schema.ends_with("_") {
523            return false;
524        }
525        // Don't allow spec names-in-schema to not be valid GraphQL names. Note that unlike
526        // `add_link()`, we consider "." and "-" to not be valid here, but since this method is only
527        // used in merging to detect whether we need to rename the spec, this has the effect of
528        // ensuring that supergraph schemas don't use "." and "-" in their spec names-in-schema
529        // (which will help later if we want to fully forbid "." and "-" in spec names-in-schema).
530        if !GRAPHQL_NAME_REGEX.is_match(spec_name_in_schema) {
531            return false;
532        }
533        for (conflict_identity, import_conflicts) in import_conflicts_by_identity {
534            if identity == conflict_identity {
535                // For import names namespaced using this spec name-in-schema, only allow them if
536                // they're no-op imports.
537                if import_conflicts.self_.contains(spec_name_in_schema) {
538                    return false;
539                }
540            } else {
541                // Don't allow imports of other specs that are namespaced by this spec
542                // name-in-schema.
543                if import_conflicts.other.contains(spec_name_in_schema) {
544                    return false;
545                }
546            }
547        }
548        // The spec name-in-schema can't be already mapped to another @link/`Link`.
549        if self
550            .by_spec_name_in_schema
551            .contains_key(spec_name_in_schema)
552        {
553            return false;
554        }
555        true
556    }
557
558    /// This is a method that helps us handle the case where:
559    /// 1. directives of some spec are being composed into the supergraph (due to them being Apollo
560    ///    specs or via `@composeDirective`),
561    /// 2. those directives don't actually have any conflicts, and
562    /// 3. the spec itself has spec name-in-schema conflicts when linked with the spec's name.
563    ///
564    /// For the `@composeDirective` case at least, you might think we could just use the
565    /// `@link(as:)` rename from the subgraph, but when we established `@composeDirective` we never
566    /// mandated that the spec names-in-schema be the same (just their composed directive
567    /// names-in-schema). The reason we focused on directives was that downstream consumers were
568    /// expecting certain directive names, so it makes sense to force agreement on a single name per
569    /// directive across subgraphs. As part of that, we explicitly generate imports for all those
570    /// directives, so the spec name-in-schema is never used for namespaced names via "__", and
571    /// consumers consequently don't deal or care about those spec names-in-schema much. We could
572    /// make a breaking change to force alignment on a spec name-in-schema to use in the supergraph,
573    /// but spec name-in-schema agreement likely isn't valuable enough for a breakage. More
574    /// importantly, it also doesn't really solve the case for conflicts linking Apollo specs.
575    ///
576    /// So instead, when we detect a conflict, we generate a unique spec name-in-schema. This method
577    /// does two things:
578    /// 1. Outputs data that can be used to efficiently detect import conflicts.
579    /// 2. Outputs a closure that can be used to generate unique spec names-in-schema.
580    ///
581    /// For unique spec name-in-schema computation, we compute a non-conflicting prefix by using a
582    /// trie to determine a GraphQL name that isn't a prefix of any existing names (this prefix also
583    /// doesn't use "_" outside the first character, so it should be safe for spec names-in-schema).
584    /// We then add an incrementing index to it, to account for @links that have the same spec name.
585    /// Finally, we add the spec name but without any non-letter characters.
586    pub(crate) fn compute_spec_name_in_schema_conflicts<'a>(
587        spec_conflict_infos: impl IntoIterator<Item = SpecConflictInfo<'a>>,
588        all_element_names: impl IntoIterator<Item = Name>,
589    ) -> (ImportConflictsByIdentity, UniqueSpecNameInSchema) {
590        // Generate `import_conflicts_by_identity` and track names for the trie.
591        let mut trie_names: IndexSet<Arc<str>> = all_element_names
592            .into_iter()
593            .map(|n| n.clone().into())
594            .collect();
595        let mut import_conflicts_by_identity: ImportConflictsByIdentity = Default::default();
596        for SpecConflictInfo {
597            spec_name_in_schema,
598            url,
599            imports,
600        } in spec_conflict_infos
601        {
602            trie_names.insert(spec_name_in_schema.clone());
603            let mut self_: IndexSet<_> = Default::default();
604            let mut other: IndexSet<_> = Default::default();
605            for import in imports {
606                let import_name_in_spec = &import.element;
607                let import_name_in_schema = import.name_in_schema();
608                trie_names.insert(import_name_in_schema.clone().into());
609                if let Some((split_spec_name_in_schema, split_name_in_spec)) =
610                    Self::split_prefixed_name(import_name_in_schema)
611                {
612                    if split_name_in_spec != import_name_in_spec.as_str() {
613                        // Spec name-in-schema being `split_spec_name_in_schema` would generate a
614                        // conflict due to the import not being a no-op import for a namespaced
615                        // name.
616                        self_.insert(split_spec_name_in_schema.into());
617                    }
618                    // Spec name-in-schema being `split_spec_name_in_schema` would generate a
619                    // conflict due to this import from some other identity being namespaced to it.
620                    other.insert(split_spec_name_in_schema.into());
621                }
622                if import.is_directive {
623                    if import_name_in_spec.as_str() != url.identity.name.as_ref() {
624                        // Spec name-in-schema being 'import_name_in_schema' would generate a
625                        // conflict due to the import not being a no-op import for the default
626                        // directive.
627                        self_.insert(import_name_in_schema.clone().into());
628                    }
629                    // Spec name-in-schema being `import_name_in_schema` would generate a conflict
630                    // due to this import from some other identity being the default directive for
631                    // it.
632                    other.insert(import_name_in_schema.clone().into());
633                }
634            }
635            import_conflicts_by_identity
636                .insert(url.identity.clone(), ImportConflict { self_, other });
637        }
638        // Create the closure for computing unique spec names-in-schema via a trie.
639        let mut prefix: Option<String> = None;
640        let mut index: usize = 0;
641        let compute_unique_spec_name_in_schema = move |spec_name: &str| {
642            let prefix = prefix.get_or_insert_with(|| {
643                let mut root: TrieNode = Default::default();
644                // Populate the trie.
645                for name in &trie_names {
646                    let mut node = &mut root;
647                    for char in name.chars() {
648                        node = node.children.entry(char).or_default();
649                    }
650                }
651                // This arena keeps track of nodes we've already traversed in the BFS, in addition
652                // to nodes we've queued but yet to traverse (starting at `head`). It's bounded by
653                // the number of nodes in the trie, and each traversal entry is constant size.
654                let mut nodes = vec![TrieTraversal {
655                    node: &root,
656                    // A sentinel value to indicate the root.
657                    parent: usize::MAX,
658                    char_: '\0',
659                }];
660                let mut head: usize = 0;
661                loop {
662                    let possible_chars = if head == 0 {
663                        TRIE_SPEC_NAME_IN_SCHEMA_START
664                    } else {
665                        TRIE_SPEC_NAME_IN_SCHEMA_CONTINUE
666                    };
667                    let node = nodes[head].clone();
668                    for char in possible_chars.chars() {
669                        if let Some(child) = node.node.children.get(&char) {
670                            nodes.push(TrieTraversal {
671                                node: child,
672                                parent: head,
673                                char_: char,
674                            })
675                        } else {
676                            let mut chars = vec![char];
677                            let mut cur = &node;
678                            while cur.parent != usize::MAX {
679                                chars.push(cur.char_);
680                                cur = &nodes[cur.parent];
681                            }
682                            return chars.into_iter().rev().collect();
683                        }
684                    }
685                    head += 1;
686                }
687            });
688            let suffix: String = spec_name
689                .chars()
690                .filter(|c| c.is_ascii_alphabetic())
691                .collect();
692            let unique_spec_name_in_schema = format!("{prefix}{index}{suffix}");
693            index += 1;
694            Name::new_unchecked(&unique_spec_name_in_schema)
695        };
696        (
697            import_conflicts_by_identity,
698            Box::new(compute_unique_spec_name_in_schema),
699        )
700    }
701
702    /// There's a particular pattern in Fed 1 subgraphs, where they would try to link the "tag" or
703    /// "inaccessible" specs directly instead of importing the directives from the "federation"
704    /// spec, and this can cause a conflict. This function gives a more helpful error message in
705    /// that case.
706    fn check_tag_inaccessible_conflict(
707        identity1: &Identity,
708        identity2: &Identity,
709    ) -> Result<(), FederationError> {
710        let identities: IndexSet<_> = [identity1, identity2].into_iter().collect();
711        if !identities.contains(&Identity::federation_identity()) {
712            return Ok(());
713        }
714        let (directive, identity) = if identities.contains(&Identity::tag_identity()) {
715            (Identity::TAG_NAME, Identity::tag_identity())
716        } else if identities.contains(&Identity::inaccessible_identity()) {
717            (
718                Identity::INACCESSIBLE_NAME,
719                Identity::inaccessible_identity(),
720            )
721        } else {
722            return Ok(());
723        };
724        Err(SingleFederationError::InvalidLinkDirectiveUsage {
725            message: format!(
726                r#"Please import "@{}" from the feature "{}" instead of using "{}" to avoid potential unexpected behavior in the future."#,
727                directive,
728                Identity::federation_identity(),
729                identity,
730            ),
731        }.into())
732    }
733
734    fn validate_known_element_name_in_spec(
735        element_name_in_spec: &ElementName,
736        known_element_names_in_spec: &Option<IndexSet<ElementName>>,
737    ) -> Result<(), FederationError> {
738        let Some(known_element_names_in_spec) = known_element_names_in_spec else {
739            return Ok(());
740        };
741        if known_element_names_in_spec.contains(element_name_in_spec) {
742            return Ok(());
743        };
744        let mut message_parts = vec![format!(
745            r#"Cannot import unknown element "{}"."#,
746            element_name_in_spec
747        )];
748        if !element_name_in_spec.is_directive
749            && let known_element_name_in_spec = (ElementName {
750                name: element_name_in_spec.name.clone(),
751                is_directive: true,
752            })
753            && known_element_names_in_spec.contains(&known_element_name_in_spec)
754        {
755            message_parts.push(format!(
756                r#" Did you mean directive "{}"?"#,
757                known_element_name_in_spec
758            ));
759        } else if let suggestions = suggestion_list(
760            &element_name_in_spec.to_string(),
761            known_element_names_in_spec.iter().map(ToString::to_string),
762        ) && !suggestions.is_empty()
763        {
764            // Note that the message returned by `did_you_mean()` has a leading space.
765            message_parts.push(did_you_mean(suggestions))
766        }
767        Err(SingleFederationError::InvalidLinkDirectiveUsage {
768            message: message_parts.join(""),
769        }
770        .into())
771    }
772
773    /// Assuming the [LinksMetadata] is for the given [FederationSchema], returns an error for each
774    /// used schema element with a shadowing import. A "shadowing import" occurs when an element
775    /// would normally belong to an @link application due to having a default name for its spec, but
776    /// the name-in-spec has been imported already under a different name. Note that for
777    /// backwards-compatibility reasons, we ignore shadowed types if they're only used by other
778    /// shadowed elements.
779    ///
780    /// We enforce this validation because downstream code almost always assumes there's exactly one
781    /// name for a spec element, and allowing multiple elements with the same @link application and
782    /// name-in-spec will thus result in some of those elements being erroneously ignored. (This is
783    /// similar to the validation that forbids importing the same name-in-spec with different
784    /// names-in-schema, but that can't be easily done when recomputing [LinksMetadata] due to a
785    /// change in @link applications since it depends on what elements are actually in the schema,
786    /// and that doesn't get finalized until later in the schema-building process.)
787    pub(crate) fn validate_no_shadowing_imports(
788        &self,
789        schema: &FederationSchema,
790    ) -> Result<(), FederationError> {
791        let mut used_shadowing_imports: Vec<(ShadowingImport, String)> = Vec::new();
792        for type_pos in schema.get_types() {
793            let element_name_in_schema = ElementName {
794                name: type_pos.type_name().clone(),
795                is_directive: false,
796            };
797            let Some(shadowing_import) = self.get_shadowing_import(&element_name_in_schema) else {
798                continue;
799            };
800            if self.has_non_shadowed_referencing_root_elements(schema, &type_pos) {
801                used_shadowing_imports.push((shadowing_import, type_pos.to_string()));
802            }
803        }
804        for directive_pos in schema.get_directive_definitions() {
805            let element_name_in_schema = ElementName {
806                name: directive_pos.directive_name.clone(),
807                is_directive: true,
808            };
809            let Some(shadowing_import) = self.get_shadowing_import(&element_name_in_schema) else {
810                continue;
811            };
812            if !schema
813                .referencers()
814                .get_directive(&directive_pos.directive_name)
815                .is_empty()
816            {
817                used_shadowing_imports.push((shadowing_import, directive_pos.to_string()));
818            };
819        }
820        if used_shadowing_imports.is_empty() {
821            return Ok(());
822        }
823        Err(FederationError::MultipleFederationErrors(MultipleFederationErrors {
824            errors: used_shadowing_imports.iter().map(|(shadowing_import, coordinate)| {
825                let import_error_message = if shadowing_import.import_element_name_in_spec ==
826                    shadowing_import.import_element_name_in_schema
827                {
828                    format!(r#""{}""#, shadowing_import.import_element_name_in_spec)
829                } else {
830                    format!(
831                        r#""{}" as "{}""#,
832                        shadowing_import.import_element_name_in_spec,
833                        shadowing_import.import_element_name_in_schema
834                    )
835                };
836                SingleFederationError::InvalidLinkDirectiveUsage {
837                    message: format!(
838                        r#"Cannot import {} from feature "{}" since there's a used definition for the namespaced name "{}". Please switch usages of the namespaced name to the import name and remove the definition."#,
839                        import_error_message,
840                        shadowing_import.link.url.identity,
841                        coordinate,
842                    )
843                }
844            }).collect(),
845        }))
846    }
847
848    fn has_non_shadowed_referencing_root_elements(
849        &self,
850        schema: &FederationSchema,
851        type_definition_position: &TypeDefinitionPosition,
852    ) -> bool {
853        match type_definition_position {
854            TypeDefinitionPosition::Scalar(type_pos) => {
855                let Some(referencers) = schema.referencers().scalar_types.get(&type_pos.type_name)
856                else {
857                    return false;
858                };
859                referencers
860                    .object_fields
861                    .iter()
862                    .map(|field_pos| ElementName {
863                        name: field_pos.type_name.clone(),
864                        is_directive: false,
865                    })
866                    .chain(
867                        referencers
868                            .object_field_arguments
869                            .iter()
870                            .map(|arg_pos| ElementName {
871                                name: arg_pos.type_name.clone(),
872                                is_directive: false,
873                            }),
874                    )
875                    .chain(
876                        referencers
877                            .interface_fields
878                            .iter()
879                            .map(|field_pos| ElementName {
880                                name: field_pos.type_name.clone(),
881                                is_directive: false,
882                            }),
883                    )
884                    .chain(referencers.interface_field_arguments.iter().map(|arg_pos| {
885                        ElementName {
886                            name: arg_pos.type_name.clone(),
887                            is_directive: false,
888                        }
889                    }))
890                    .chain(
891                        referencers
892                            .union_fields
893                            .iter()
894                            .map(|field_pos| ElementName {
895                                name: field_pos.type_name.clone(),
896                                is_directive: false,
897                            }),
898                    )
899                    .chain(
900                        referencers
901                            .input_object_fields
902                            .iter()
903                            .map(|field_pos| ElementName {
904                                name: field_pos.type_name.clone(),
905                                is_directive: false,
906                            }),
907                    )
908                    .chain(
909                        referencers
910                            .directive_arguments
911                            .iter()
912                            .map(|arg_pos| ElementName {
913                                name: arg_pos.directive_name.clone(),
914                                is_directive: true,
915                            }),
916                    )
917                    .any(|element_name_in_schema| {
918                        self.get_shadowing_import(&element_name_in_schema).is_none()
919                    })
920            }
921            TypeDefinitionPosition::Object(type_pos) => {
922                let Some(referencers) = schema.referencers().object_types.get(&type_pos.type_name)
923                else {
924                    return false;
925                };
926                if !referencers.schema_roots.is_empty() {
927                    return true;
928                }
929                referencers
930                    .object_fields
931                    .iter()
932                    .map(|field_pos| ElementName {
933                        name: field_pos.type_name.clone(),
934                        is_directive: false,
935                    })
936                    .chain(
937                        referencers
938                            .interface_fields
939                            .iter()
940                            .map(|field_pos| ElementName {
941                                name: field_pos.type_name.clone(),
942                                is_directive: false,
943                            }),
944                    )
945                    .chain(referencers.union_types.iter().map(|type_pos| ElementName {
946                        name: type_pos.type_name.clone(),
947                        is_directive: false,
948                    }))
949                    .any(|element_name_in_schema| {
950                        self.get_shadowing_import(&element_name_in_schema).is_none()
951                    })
952            }
953            TypeDefinitionPosition::Interface(type_pos) => {
954                let Some(referencers) = schema
955                    .referencers()
956                    .interface_types
957                    .get(&type_pos.type_name)
958                else {
959                    return false;
960                };
961                referencers
962                    .object_types
963                    .iter()
964                    .map(|type_pos| ElementName {
965                        name: type_pos.type_name.clone(),
966                        is_directive: false,
967                    })
968                    .chain(
969                        referencers
970                            .object_fields
971                            .iter()
972                            .map(|field_pos| ElementName {
973                                name: field_pos.type_name.clone(),
974                                is_directive: false,
975                            }),
976                    )
977                    .chain(
978                        referencers
979                            .interface_types
980                            .iter()
981                            .map(|type_pos| ElementName {
982                                name: type_pos.type_name.clone(),
983                                is_directive: false,
984                            }),
985                    )
986                    .chain(
987                        referencers
988                            .interface_fields
989                            .iter()
990                            .map(|field_pos| ElementName {
991                                name: field_pos.type_name.clone(),
992                                is_directive: false,
993                            }),
994                    )
995                    .any(|element_name_in_schema| {
996                        self.get_shadowing_import(&element_name_in_schema).is_none()
997                    })
998            }
999            TypeDefinitionPosition::Union(type_pos) => {
1000                let Some(referencers) = schema.referencers().union_types.get(&type_pos.type_name)
1001                else {
1002                    return false;
1003                };
1004                referencers
1005                    .object_fields
1006                    .iter()
1007                    .map(|field_pos| ElementName {
1008                        name: field_pos.type_name.clone(),
1009                        is_directive: false,
1010                    })
1011                    .chain(
1012                        referencers
1013                            .interface_fields
1014                            .iter()
1015                            .map(|field_pos| ElementName {
1016                                name: field_pos.type_name.clone(),
1017                                is_directive: false,
1018                            }),
1019                    )
1020                    .any(|element_name_in_schema| {
1021                        self.get_shadowing_import(&element_name_in_schema).is_none()
1022                    })
1023            }
1024            TypeDefinitionPosition::Enum(type_pos) => {
1025                let Some(referencers) = schema.referencers().enum_types.get(&type_pos.type_name)
1026                else {
1027                    return false;
1028                };
1029                referencers
1030                    .object_fields
1031                    .iter()
1032                    .map(|field_pos| ElementName {
1033                        name: field_pos.type_name.clone(),
1034                        is_directive: false,
1035                    })
1036                    .chain(
1037                        referencers
1038                            .object_field_arguments
1039                            .iter()
1040                            .map(|arg_pos| ElementName {
1041                                name: arg_pos.type_name.clone(),
1042                                is_directive: false,
1043                            }),
1044                    )
1045                    .chain(
1046                        referencers
1047                            .interface_fields
1048                            .iter()
1049                            .map(|field_pos| ElementName {
1050                                name: field_pos.type_name.clone(),
1051                                is_directive: false,
1052                            }),
1053                    )
1054                    .chain(referencers.interface_field_arguments.iter().map(|arg_pos| {
1055                        ElementName {
1056                            name: arg_pos.type_name.clone(),
1057                            is_directive: false,
1058                        }
1059                    }))
1060                    .chain(
1061                        referencers
1062                            .input_object_fields
1063                            .iter()
1064                            .map(|field_pos| ElementName {
1065                                name: field_pos.type_name.clone(),
1066                                is_directive: false,
1067                            }),
1068                    )
1069                    .chain(
1070                        referencers
1071                            .directive_arguments
1072                            .iter()
1073                            .map(|arg_pos| ElementName {
1074                                name: arg_pos.directive_name.clone(),
1075                                is_directive: true,
1076                            }),
1077                    )
1078                    .any(|element_name_in_schema| {
1079                        self.get_shadowing_import(&element_name_in_schema).is_none()
1080                    })
1081            }
1082            TypeDefinitionPosition::InputObject(type_pos) => {
1083                let Some(referencers) = schema
1084                    .referencers()
1085                    .input_object_types
1086                    .get(&type_pos.type_name)
1087                else {
1088                    return false;
1089                };
1090                referencers
1091                    .object_field_arguments
1092                    .iter()
1093                    .map(|arg_pos| ElementName {
1094                        name: arg_pos.type_name.clone(),
1095                        is_directive: false,
1096                    })
1097                    .chain(referencers.interface_field_arguments.iter().map(|arg_pos| {
1098                        ElementName {
1099                            name: arg_pos.type_name.clone(),
1100                            is_directive: false,
1101                        }
1102                    }))
1103                    .chain(
1104                        referencers
1105                            .input_object_fields
1106                            .iter()
1107                            .map(|field_pos| ElementName {
1108                                name: field_pos.type_name.clone(),
1109                                is_directive: false,
1110                            }),
1111                    )
1112                    .chain(
1113                        referencers
1114                            .directive_arguments
1115                            .iter()
1116                            .map(|arg_pos| ElementName {
1117                                name: arg_pos.directive_name.clone(),
1118                                is_directive: true,
1119                            }),
1120                    )
1121                    .any(|element_name_in_schema| {
1122                        self.get_shadowing_import(&element_name_in_schema).is_none()
1123                    })
1124            }
1125        }
1126    }
1127
1128    fn get_shadowing_import(
1129        &self,
1130        element_name_in_schema: &ElementName,
1131    ) -> Option<ShadowingImport<'_>> {
1132        let (link, element_name_in_spec) = self.source_default_name(element_name_in_schema)?;
1133        // An invalid name-in-spec can't be imported, and thus can't be shadowed.
1134        let element_name_in_spec = element_name_in_spec?;
1135        let import_element_name_in_schema =
1136            self.get_import_element_name_in_schema(link, &element_name_in_spec)?;
1137        // If the import is a no-op import (i.e. it imports an element to its default name), then
1138        // we don't consider it to shadow usages of the default name.
1139        if import_element_name_in_schema == element_name_in_schema {
1140            return None;
1141        }
1142        Some(ShadowingImport {
1143            link,
1144            import_element_name_in_spec: element_name_in_spec,
1145            import_element_name_in_schema: import_element_name_in_schema.clone(),
1146        })
1147    }
1148
1149    // PORT_NOTE: Named `coreItself` in the JS codebase, but "core" is outdated terminology.
1150    pub(crate) fn link_itself(&self) -> &Arc<Link> {
1151        &self.link_itself
1152    }
1153
1154    pub(crate) fn link_spec_definition(&self) -> &'static LinkSpecDefinition {
1155        self.link_spec_definition
1156    }
1157
1158    pub(crate) fn by_import_element_name_in_schema(
1159        &self,
1160    ) -> &IndexMap<ElementName, (Arc<Link>, ElementName)> {
1161        &self.by_import_element_name_in_schema
1162    }
1163
1164    // PORT_NOTE: Named `allFeatures` in the JS codebase, but "feature" is outdated terminology.
1165    pub fn all_links(&self) -> impl ExactSizeIterator<Item = &Arc<Link>> {
1166        self.by_identity.values().map(|(link, _)| link)
1167    }
1168
1169    pub fn for_identity(&self, identity: &Identity) -> Option<Arc<Link>> {
1170        self.by_identity.get(identity).map(|(link, _)| link.clone())
1171    }
1172
1173    pub fn source_link_of_type(&self, type_name: &Name) -> Option<LinkedElement> {
1174        let element_name_in_schema = ElementName {
1175            name: type_name.clone(),
1176            is_directive: false,
1177        };
1178        self.source_link(&element_name_in_schema)
1179    }
1180
1181    pub fn source_link_of_directive(&self, directive_name: &Name) -> Option<LinkedElement> {
1182        let element_name_in_schema = ElementName {
1183            name: directive_name.clone(),
1184            is_directive: true,
1185        };
1186        self.source_link(&element_name_in_schema)
1187    }
1188
1189    // PORT_NOTE: Named `sourceFeature` in the JS codebase, but "feature" is outdated terminology.
1190    fn source_link(&self, element_name_in_schema: &ElementName) -> Option<LinkedElement> {
1191        // Validations guarantee that names-in-schema don't collide with the default names of
1192        // different spec schema elements, so it doesn't technically matter which order we check
1193        // first. But we do have to some extra work for shadowing imports if we don't check imports
1194        // first, so we do that first.
1195        if let Some((link, import_element_name_in_spec)) = self
1196            .by_import_element_name_in_schema
1197            .get(element_name_in_schema)
1198        {
1199            return Some(LinkedElement {
1200                link: link.clone(),
1201                name_in_spec: Some(import_element_name_in_spec.name.clone()),
1202            });
1203        }
1204        // If it's not an import, check whether it's a default name for some existing spec.
1205        let (link, element_name_in_spec) = self.source_default_name(element_name_in_schema)?;
1206        Some(LinkedElement {
1207            link: link.clone(),
1208            name_in_spec: element_name_in_spec.and_then(|element_name_in_spec| {
1209                // Note that if an import's name-in-schema is the same as its default name, it's not
1210                // a shadowing import, and we should return a `Some` for `name_in_spec`. But if that
1211                // were true, we would have found an entry in `self.by_element_name_in_schema` above
1212                // when checking for imports. So we don't need to handle that case specially here.
1213                if self
1214                    .get_import_element_name_in_schema(link, &element_name_in_spec)
1215                    .is_none()
1216                {
1217                    Some(element_name_in_spec.name)
1218                } else {
1219                    None
1220                }
1221            }),
1222        })
1223    }
1224
1225    /// Returns the name-in-schema of an imported type/directive for the given link and
1226    /// name-in-spec.
1227    // PORT_NOTE: Named `getImportName` in the JS codebase, but this was vague.
1228    fn get_import_element_name_in_schema(
1229        &self,
1230        link: &Arc<Link>,
1231        element_name_in_spec: &ElementName,
1232    ) -> Option<&ElementName> {
1233        self.by_identity
1234            .get(&link.url.identity)
1235            .and_then(|(_, by_name_in_spec)| by_name_in_spec.get(element_name_in_spec))
1236    }
1237
1238    /// If the given element name is a default name (i.e. it's prefixed with an existing spec
1239    /// name-in-schema, or is a directive name for an existing spec name-in-schema), then return the
1240    /// [Link] for that spec along with the name-in-spec for the element (if valid).
1241    fn source_default_name(
1242        &self,
1243        element_name_in_schema: &ElementName,
1244    ) -> Option<(&Arc<Link>, Option<ElementName>)> {
1245        // Handle the prefixed case first.
1246        if let Some((spec_name_in_schema, name_in_spec)) =
1247            Self::split_prefixed_name(&element_name_in_schema.name)
1248        {
1249            // Note that we explicitly do not return `None` here if `link` isn't found, and instead
1250            // fall-through to the default directive name logic below. Normally that default
1251            // directive name logic would also return `None`, since validations above guarantee "__"
1252            // isn't in spec names-in-schema. But as noted above, we make an exception for the "tag"
1253            // and "inaccessible" specs for backwards-compatibility reasons, so we fall-through to
1254            // allow those exceptions to be found in `self.by_spec_name_in_schema`.
1255            if let Some(link) = self.by_spec_name_in_schema.get(spec_name_in_schema) {
1256                return Some((
1257                    link,
1258                    Name::new(name_in_spec).ok().map(|name| ElementName {
1259                        name,
1260                        is_directive: element_name_in_schema.is_directive,
1261                    }),
1262                ));
1263            }
1264        }
1265        // If not prefixed, then check whether it's the default directive name for a spec.
1266        if !element_name_in_schema.is_directive {
1267            return None;
1268        }
1269        let link = self
1270            .by_spec_name_in_schema
1271            .get(element_name_in_schema.name.as_str())?;
1272        let name_in_spec = link.url.identity.name.clone().try_into().ok();
1273        Some((
1274            link,
1275            name_in_spec.map(|name| ElementName {
1276                name,
1277                is_directive: true,
1278            }),
1279        ))
1280    }
1281
1282    /// For type/directive names-in-schema that are prefixed to indicate they belong to a spec,
1283    /// return their spec name-in-schema and their type/directive name-in-spec (which may not be
1284    /// a valid GraphQL name).
1285    fn split_prefixed_name(name_in_schema: &Name) -> Option<(&str, &str)> {
1286        name_in_schema.split_once("__")
1287    }
1288}
1289
1290pub(crate) type ImportConflictsByIdentity = IndexMap<Identity, ImportConflict>;
1291
1292pub(crate) struct ImportConflict {
1293    /// Spec names-in-schema that would conflict with this identity.
1294    self_: IndexSet<Arc<str>>,
1295    /// Spec names-in-schema that would conflict with other identities than this one.
1296    other: IndexSet<Arc<str>>,
1297}
1298
1299pub(crate) struct SpecConflictInfo<'a> {
1300    pub(crate) spec_name_in_schema: &'a Arc<str>,
1301    pub(crate) url: &'a Url,
1302    pub(crate) imports: Box<dyn Iterator<Item = Cow<'a, Import>> + 'a>,
1303}
1304
1305pub(crate) type UniqueSpecNameInSchema = Box<dyn FnMut(&str) -> Name>;
1306
1307/// Information about a shadowing import found for an element.
1308struct ShadowingImport<'a> {
1309    link: &'a Arc<Link>,
1310    import_element_name_in_spec: ElementName,
1311    import_element_name_in_schema: ElementName,
1312}
1313
1314#[derive(Default)]
1315struct TrieNode {
1316    children: HashMap<char, TrieNode>,
1317}
1318
1319#[derive(Clone)]
1320struct TrieTraversal<'a> {
1321    node: &'a TrieNode,
1322    parent: usize,
1323    char_: char,
1324}
1325
1326static SPEC_NAME_IN_SCHEMA_REGEX: LazyLock<regex::Regex> =
1327    LazyLock::new(|| regex::Regex::new(r#"^[_A-Za-z][_0-9A-Za-z.\-]*$"#).unwrap());
1328
1329static GRAPHQL_NAME_REGEX: LazyLock<regex::Regex> =
1330    LazyLock::new(|| regex::Regex::new(r#"^[_A-Za-z][_0-9A-Za-z]*$"#).unwrap());
1331
1332static TRIE_SPEC_NAME_IN_SCHEMA_START: &str = concat!(
1333    "_",
1334    "abcdefghijklmnopqrstuvwxyz",
1335    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1336);
1337
1338static TRIE_SPEC_NAME_IN_SCHEMA_CONTINUE: &str = concat!(
1339    "abcdefghijklmnopqrstuvwxyz",
1340    "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
1341    "0123456789"
1342);
1343
1344#[cfg(test)]
1345mod tests {
1346    use std::collections::BTreeSet;
1347
1348    use apollo_compiler::name;
1349
1350    use super::*;
1351    use crate::link::Import;
1352    use crate::link::Purpose;
1353    use crate::link::spec::Version;
1354    use crate::subgraph::typestate::Subgraph;
1355
1356    fn errors(schema: &str) -> Vec<String> {
1357        // Note: we use `expand_links()` because currently it takes care of automatically adding
1358        // directive definitions, and we don't want to bother with adding the @link definition
1359        // to every example.
1360        let actual_errors: BTreeSet<_> =
1361            match Subgraph::parse("A", "", schema).and_then(|subgraph| subgraph.expand_links()) {
1362                Ok(_) => Default::default(),
1363                Err(error) => error
1364                    .errors
1365                    .into_iter()
1366                    .map(|e| e.error.to_string())
1367                    .collect(),
1368            };
1369        actual_errors.into_iter().collect()
1370    }
1371
1372    #[test]
1373    fn explicit_root_directive_import() -> Result<(), FederationError> {
1374        let schema = r#"
1375          extend schema
1376            @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import"])
1377            @link(url: "https://specs.apollo.dev/inaccessible/v0.2", import: ["@inaccessible"])
1378
1379          type Query { x: Int }
1380
1381          enum link__Purpose {
1382            SECURITY
1383            EXECUTION
1384          }
1385
1386          scalar Import
1387
1388          directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
1389        "#;
1390
1391        let schema = Schema::parse(schema, "root_directive.graphqls").unwrap();
1392
1393        let meta = LinksMetadata::from_schema(&schema)?;
1394        let meta = meta.expect("should have metadata");
1395
1396        assert!(
1397            meta.source_link_of_directive(&name!("inaccessible"))
1398                .is_some()
1399        );
1400
1401        Ok(())
1402    }
1403
1404    #[test]
1405    fn renamed_link_directive() -> Result<(), FederationError> {
1406        let schema = r#"
1407          extend schema
1408            @lonk(url: "https://specs.apollo.dev/link/v1.0", as: "lonk")
1409            @lonk(url: "https://specs.apollo.dev/inaccessible/v0.2")
1410
1411          type Query { x: Int }
1412
1413          enum lonk__Purpose {
1414            SECURITY
1415            EXECUTION
1416          }
1417
1418          scalar lonk__Import
1419
1420          directive @lonk(url: String!, as: String, import: [lonk__Import], for: lonk__Purpose) repeatable on SCHEMA
1421        "#;
1422
1423        let schema = Schema::parse(schema, "lonk.graphqls").unwrap();
1424
1425        let meta = LinksMetadata::from_schema(&schema)?.expect("should have metadata");
1426        assert!(
1427            meta.source_link_of_directive(&name!("inaccessible"))
1428                .is_some()
1429        );
1430
1431        Ok(())
1432    }
1433
1434    #[test]
1435    fn renamed_core_directive() -> Result<(), FederationError> {
1436        let schema = r#"
1437          extend schema
1438            @care(feature: "https://specs.apollo.dev/core/v0.2", as: "care")
1439            @care(feature: "https://specs.apollo.dev/join/v0.2", for: EXECUTION)
1440
1441          directive @care(feature: String!, as: String, for: core__Purpose) repeatable on SCHEMA
1442          directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1443          directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1444          directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1445          directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1446
1447          type Query { x: Int }
1448
1449          enum care__Purpose {
1450            SECURITY
1451            EXECUTION
1452          }
1453
1454          scalar care__Import
1455
1456          scalar join__FieldSet
1457
1458          enum join__Graph {
1459            USERS @join__graph(name: "users", url: "http://localhost:4001")
1460          }
1461        "#;
1462
1463        let schema = Schema::parse(schema, "care.graphqls").unwrap();
1464
1465        let meta = LinksMetadata::from_schema(&schema)?.expect("should have metadata");
1466        assert!(
1467            meta.source_link_of_directive(&name!("join__graph"))
1468                .is_some()
1469        );
1470
1471        Ok(())
1472    }
1473
1474    #[test]
1475    fn url_syntax() -> Result<(), FederationError> {
1476        let schema = r#"
1477            extend schema
1478              @link(url: "https://specs.apollo.dev/link/v1.0")
1479              @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1480              @link(url: "https://example.com/my-directive/v1.0", import: ["@myDirective"])
1481
1482          type Query { x: Int }
1483
1484            directive @myDirective on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
1485
1486            directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1487
1488            directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1489
1490            directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1491
1492            directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1493
1494            directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1495
1496            directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1497
1498            directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1499        "#;
1500
1501        let schema = Schema::parse(schema, "url_dash.graphqls").unwrap();
1502
1503        let meta = LinksMetadata::from_schema(&schema)?;
1504        let meta = meta.expect("should have metadata");
1505
1506        assert!(
1507            meta.source_link_of_directive(&name!("myDirective"))
1508                .is_some()
1509        );
1510
1511        Ok(())
1512    }
1513
1514    #[test]
1515    fn computes_link_metadata() {
1516        let schema = r#"
1517          extend schema
1518            @link(url: "https://specs.apollo.dev/link/v1.0", import: ["Import"])
1519            @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", { name: "@tag", as: "@myTag" }])
1520            @link(url: "https://custom.com/someSpec/v0.2", as: "mySpec")
1521            @link(url: "https://megacorp.com/auth/v1.0", for: SECURITY)
1522
1523          type Query {
1524            x: Int
1525          }
1526
1527          enum link__Purpose {
1528            SECURITY
1529            EXECUTION
1530          }
1531
1532          scalar Import
1533
1534          directive @link(url: String, as: String, import: [Import], for: link__Purpose) repeatable on SCHEMA
1535        "#;
1536
1537        let schema = Schema::parse(schema, "testSchema").unwrap();
1538
1539        let meta = LinksMetadata::from_schema(&schema)
1540            // TODO: error handling?
1541            .unwrap()
1542            .unwrap();
1543        let names_in_schema = meta
1544            .all_links()
1545            .map(|l| l.spec_name_in_schema())
1546            .collect::<Vec<_>>();
1547        assert_eq!(names_in_schema.len(), 4);
1548        assert_eq!(names_in_schema[0], "link");
1549        assert_eq!(names_in_schema[1], "federation");
1550        assert_eq!(names_in_schema[2], "mySpec");
1551        assert_eq!(names_in_schema[3], "auth");
1552
1553        let link_spec = meta.for_identity(&Identity::link_identity()).unwrap();
1554        assert_eq!(
1555            link_spec.imports.first().unwrap().as_ref(),
1556            &Import {
1557                element: name!("Import"),
1558                is_directive: false,
1559                alias: None
1560            }
1561        );
1562
1563        let fed_spec = meta.for_identity(&Identity::federation_identity()).unwrap();
1564        assert_eq!(fed_spec.url.version, Version { major: 2, minor: 3 });
1565        assert_eq!(fed_spec.purpose, None);
1566
1567        let imports = &fed_spec.imports;
1568        assert_eq!(imports.len(), 2);
1569        assert_eq!(
1570            imports.first().unwrap().as_ref(),
1571            &Import {
1572                element: name!("key"),
1573                is_directive: true,
1574                alias: None
1575            }
1576        );
1577        assert_eq!(
1578            imports.get(1).unwrap().as_ref(),
1579            &Import {
1580                element: name!("tag"),
1581                is_directive: true,
1582                alias: Some(name!("myTag"))
1583            }
1584        );
1585
1586        let auth_spec = meta
1587            .for_identity(&Identity {
1588                domain: "https://megacorp.com".to_string(),
1589                name: name!("auth").into(),
1590            })
1591            .unwrap();
1592        assert_eq!(auth_spec.purpose, Some(Purpose::SECURITY));
1593
1594        let import_source = meta.source_link_of_type(&name!("Import")).unwrap();
1595        assert_eq!(import_source.link.url.identity.name.as_ref(), "link");
1596        assert_eq!(import_source.name_in_spec, Some(name!("Import")));
1597
1598        // Purpose is not imported, so it should only be accessible in fql form
1599        assert!(meta.source_link_of_type(&name!("Purpose")).is_none());
1600
1601        let purpose_source = meta.source_link_of_type(&name!("link__Purpose")).unwrap();
1602        assert_eq!(purpose_source.link.url.identity.name.as_ref(), "link");
1603        assert_eq!(purpose_source.name_in_spec, Some(name!("Purpose")));
1604
1605        let key_source = meta.source_link_of_directive(&name!("key")).unwrap();
1606        assert_eq!(key_source.link.url.identity.name.as_ref(), "federation");
1607        assert_eq!(key_source.name_in_spec, Some(name!("key")));
1608
1609        // tag is imported under an alias, so "tag" itself should not match
1610        assert!(meta.source_link_of_directive(&name!("tag")).is_none());
1611
1612        let tag_source = meta.source_link_of_directive(&name!("myTag")).unwrap();
1613        assert_eq!(tag_source.link.url.identity.name.as_ref(), "federation");
1614        assert_eq!(tag_source.name_in_spec, Some(name!("tag")));
1615    }
1616
1617    /// TODO: In the JS codebase, malformed imports can generate multiple errors, we should mirror
1618    ///       that behavior eventually.
1619    mod link_import {
1620        use super::*;
1621
1622        #[test]
1623        fn errors_on_malformed_values() {
1624            insta::assert_debug_snapshot!(errors(r#"
1625              extend schema @link(
1626                url: "https://specs.apollo.dev/federation/v2.0",
1627                import: [2]
1628              )
1629
1630              type Query {
1631                q: Int
1632              }
1633            "#), @r###"
1634            [
1635                "`2` in @link(import:) argument must either be a string `\"<importedElement>\"` or an object `{ name: \"<importedElement>\", as: \"<alias>\" }`",
1636            ]
1637            "###);
1638            insta::assert_debug_snapshot!(errors(r#"
1639              extend schema @link(
1640                url: "https://specs.apollo.dev/federation/v2.0",
1641                import: [{ foo: "bar" }]
1642              )
1643
1644              type Query {
1645                q: Int
1646              }
1647            "#), @r###"
1648            [
1649                "For `{foo: \"bar\"}` in @link(import:) argument, field \"foo\" is not a known field",
1650            ]
1651            "###);
1652            insta::assert_debug_snapshot!(errors(r#"
1653              extend schema @link(
1654                url: "https://specs.apollo.dev/federation/v2.0",
1655                import: [{ name: "@key", badName: "foo" }]
1656              )
1657
1658              type Query {
1659                q: Int
1660              }
1661            "#), @r###"
1662            [
1663                "For `{name: \"@key\", badName: \"foo\"}` in @link(import:) argument, field \"badName\" is not a known field",
1664            ]
1665            "###);
1666            insta::assert_debug_snapshot!(errors(r#"
1667              extend schema @link(
1668                url: "https://specs.apollo.dev/federation/v2.0",
1669                import: [{ name: 42 }]
1670              )
1671
1672              type Query {
1673                q: Int
1674              }
1675            "#), @r###"
1676            [
1677                "For `{name: 42}` in @link(import:) argument, value for field \"name\" must be a string",
1678            ]
1679            "###);
1680            insta::assert_debug_snapshot!(errors(r#"
1681              extend schema @link(
1682                url: "https://specs.apollo.dev/federation/v2.0",
1683                import: [{ name: "42" }]
1684              )
1685
1686              type Query {
1687                q: Int
1688              }
1689            "#), @r###"
1690            [
1691                "For `{name: \"42\"}` in @link(import:) argument, value for field \"name\" is not a valid GraphQL name",
1692            ]
1693            "###);
1694            insta::assert_debug_snapshot!(errors(r#"
1695              extend schema @link(
1696                url: "https://specs.apollo.dev/federation/v2.0",
1697                import: [{ name: "" }]
1698              )
1699
1700              type Query {
1701                q: Int
1702              }
1703            "#), @r###"
1704            [
1705                "For `{name: \"\"}` in @link(import:) argument, value for field \"name\" is not a valid GraphQL name",
1706            ]
1707            "###);
1708            insta::assert_debug_snapshot!(errors(r#"
1709              extend schema @link(
1710                url: "https://specs.apollo.dev/federation/v2.0",
1711                import: [{ name: "@bar", as: "@" }]
1712              )
1713
1714              type Query {
1715                q: Int
1716              }
1717            "#), @r###"
1718            [
1719                "For `{name: \"@bar\", as: \"@\"}` in @link(import:) argument, value for field \"as\" is not a valid GraphQL name",
1720            ]
1721            "###);
1722            insta::assert_debug_snapshot!(errors(r#"
1723              extend schema @link(
1724                url: "https://specs.apollo.dev/federation/v2.0",
1725                import: [{ as: "bar" }]
1726              )
1727
1728              type Query {
1729                q: Int
1730              }
1731            "#), @r###"
1732            [
1733                "For `{as: \"bar\"}` in @link(import:) argument, missing required field \"name\"",
1734            ]
1735            "###);
1736        }
1737
1738        #[test]
1739        fn errors_on_mismatch_between_name_and_alias() {
1740            insta::assert_debug_snapshot!(errors(r#"
1741              extend schema @link(
1742                url: "https://specs.apollo.dev/federation/v2.0",
1743                import: [{ name: "@key", as: "myKey" }]
1744              )
1745
1746              type Query {
1747                q: Int
1748              }
1749            "#), @r###"
1750            [
1751                "For `{name: \"@key\", as: \"myKey\"}` in @link(import:) argument, value for field \"as\" must start with \"@\" since value for field \"name\" does (\"@\" indicates a directive import)",
1752            ]
1753            "###);
1754            insta::assert_debug_snapshot!(errors(r#"
1755              extend schema @link(
1756                url: "https://specs.apollo.dev/federation/v2.0",
1757                import: [{ name: "FieldSet", as: "@fieldSet" }]
1758              )
1759
1760              type Query {
1761                q: Int
1762              }
1763            "#), @r###"
1764            [
1765                "For `{name: \"FieldSet\", as: \"@fieldSet\"}` in @link(import:) argument, value for field \"as\" must not start with \"@\" since value for field \"name\" does not (\"@\" indicates a directive import)",
1766            ]
1767            "###);
1768        }
1769
1770        #[test]
1771        fn errors_on_importing_unknown_elements_for_known_features() {
1772            insta::assert_debug_snapshot!(errors(r#"
1773              extend schema @link(
1774                url: "https://specs.apollo.dev/federation/v2.0",
1775                import: ["@foo"]
1776              )
1777
1778              type Query {
1779                q: Int
1780              }
1781            "#), @r###"
1782            [
1783                "Cannot import unknown element \"@foo\".",
1784            ]
1785            "###);
1786            insta::assert_debug_snapshot!(errors(r#"
1787              extend schema @link(
1788                url: "https://specs.apollo.dev/federation/v2.0",
1789                import: ["key"]
1790              )
1791
1792              type Query {
1793                q: Int
1794              }
1795            "#), @r###"
1796            [
1797                "Cannot import unknown element \"key\". Did you mean directive \"@key\"?",
1798            ]
1799            "###);
1800            insta::assert_debug_snapshot!(errors(r#"
1801              extend schema @link(
1802                url: "https://specs.apollo.dev/federation/v2.0",
1803                import: [{ name: "@sharable" }]
1804              )
1805
1806              type Query {
1807                q: Int
1808              }
1809            "#), @r###"
1810            [
1811                "Cannot import unknown element \"@sharable\". Did you mean \"@shareable\"?",
1812            ]
1813            "###);
1814        }
1815    }
1816
1817    mod link_alias_and_import_conflicts {
1818        use super::*;
1819
1820        #[test]
1821        fn errors_for_same_identity_imported_twice() {
1822            insta::assert_debug_snapshot!(errors(r#"
1823              extend schema
1824                @link(url: "https://specs.apollo.dev/federation/v2.0")
1825                @link(url: "https://specs.apollo.dev/federation/v2.0")
1826
1827              type Query {
1828                q: Int
1829              }
1830            "#), @r###"
1831            [
1832                "Cannot link feature \"https://specs.apollo.dev/federation\" since it has already been linked in the schema.",
1833            ]
1834            "###);
1835        }
1836
1837        #[test]
1838        fn errors_for_spec_name_in_schema_containing_double_underscore() {
1839            insta::assert_debug_snapshot!(errors(r#"
1840              extend schema
1841                @link(url: "https://specs.apollo.dev/federation/v2.0")
1842                @link(url: "https://custom.dev/f__oo/v1.0")
1843
1844              type Query {
1845                q: Int
1846              }
1847            "#), @r###"
1848            [
1849                "Cannot link feature \"https://custom.dev/f__oo\" as \"f__oo\" since it contains \"__\". Please rename to a compliant name via \"as\".",
1850            ]
1851            "###);
1852        }
1853
1854        #[test]
1855        fn succeeds_renaming_spec_name_containing_double_underscore() {
1856            insta::assert_debug_snapshot!(errors(r#"
1857              extend schema
1858                @link(url: "https://specs.apollo.dev/federation/v2.0")
1859                @link(url: "https://custom.dev/f__oo/v1.0", as: "foo")
1860
1861              type Query {
1862                q: Int
1863              }
1864            "#), @"[]");
1865        }
1866
1867        // See the relevant code in `LinksMetadata.add_link()` for why we have this exception. That
1868        // exception and this test may be removed in the future once we have dropped support for the
1869        // bugged compositions that necessitate the exception.
1870        #[test]
1871        fn allows_exception_in_double_underscore_validation_for_federation_namespaced_tag_and_inaccessible()
1872         {
1873            insta::assert_debug_snapshot!(errors(r#"
1874              schema
1875                @link(url: "https://specs.apollo.dev/link/v1.0")
1876                @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1877                @link(
1878                  url: "https://specs.apollo.dev/inaccessible/v0.2"
1879                  as: "federation__inaccessible"
1880                  for: SECURITY
1881                )
1882                @link(url: "https://specs.apollo.dev/tag/v0.3", as: "federation__tag") {
1883                query: Query
1884              }
1885
1886              directive @federation__tag(
1887                name: String!
1888              ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
1889
1890              directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
1891
1892              directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1893
1894              directive @join__field(
1895                graph: join__Graph
1896                requires: join__FieldSet
1897                provides: join__FieldSet
1898                type: String
1899                external: Boolean
1900                override: String
1901                usedOverridden: Boolean
1902              ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1903
1904              directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1905
1906              directive @join__implements(
1907                graph: join__Graph!
1908                interface: String!
1909              ) repeatable on OBJECT | INTERFACE
1910
1911              directive @join__type(
1912                graph: join__Graph!
1913                key: join__FieldSet
1914                extension: Boolean! = false
1915                resolvable: Boolean! = true
1916                isInterfaceObject: Boolean! = false
1917              ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1918
1919              directive @join__unionMember(
1920                graph: join__Graph!
1921                member: String!
1922              ) repeatable on UNION
1923
1924              directive @link(
1925                url: String
1926                as: String
1927                for: link__Purpose
1928                import: [link__Import]
1929              ) repeatable on SCHEMA
1930
1931              scalar join__FieldSet
1932
1933              enum join__Graph {
1934                S @join__graph(name: "s", url: "")
1935              }
1936
1937              scalar link__Import
1938
1939              enum link__Purpose {
1940                SECURITY
1941                EXECUTION
1942              }
1943
1944              type Query @join__type(graph: S) {
1945                q: Int
1946              }
1947            "#), @"[]");
1948        }
1949
1950        #[test]
1951        fn errors_for_spec_name_in_schema_ending_in_underscore() {
1952            insta::assert_debug_snapshot!(errors(r#"
1953              extend schema
1954                @link(url: "https://specs.apollo.dev/federation/v2.0")
1955                @link(url: "https://custom.dev/foo_/v1.0")
1956
1957              type Query {
1958                q: Int
1959              }
1960            "#), @r###"
1961            [
1962                "Cannot link feature \"https://custom.dev/foo_\" as \"foo_\" since it ends in \"_\". Please rename to a compliant name via \"as\".",
1963            ]
1964            "###);
1965        }
1966
1967        #[test]
1968        fn succeeds_renaming_spec_name_ending_in_underscore() {
1969            insta::assert_debug_snapshot!(errors(r#"
1970              extend schema
1971                @link(url: "https://specs.apollo.dev/federation/v2.0")
1972                @link(url: "https://custom.dev/foo_/v1.0", as: "foo")
1973
1974              type Query {
1975                q: Int
1976              }
1977            "#), @"[]");
1978        }
1979
1980        #[test]
1981        fn errors_for_spec_name_in_schema_that_is_not_a_valid_graphql_name() {
1982            insta::assert_debug_snapshot!(errors(r#"
1983              extend schema
1984                @link(url: "https://specs.apollo.dev/federation/v2.0")
1985                @link(url: "https://custom.dev/0foo/v1.0")
1986
1987              type Query {
1988                q: Int
1989              }
1990            "#), @r###"
1991            [
1992                "Cannot link feature \"https://custom.dev/0foo\" as \"0foo\" since it is not a valid GraphQL name. Please rename to a compliant name via \"as\".",
1993            ]
1994            "###);
1995        }
1996
1997        #[test]
1998        fn succeeds_renaming_spec_name_that_is_not_a_valid_graphql_name() {
1999            insta::assert_debug_snapshot!(errors(r#"
2000              extend schema
2001                @link(url: "https://specs.apollo.dev/federation/v2.0")
2002                @link(url: "https://custom.dev/0foo/v1.0", as: "foo")
2003
2004              type Query {
2005                q: Int
2006              }
2007            "#), @"[]");
2008        }
2009
2010        // See the relevant code in `LinksMetadata.add_link()` for why we have this exception. That
2011        // exception and this test may be removed in the future during a major version bump.
2012        #[test]
2013        fn allows_exception_in_graphql_name_validation_for_period_and_hyphen() {
2014            insta::assert_debug_snapshot!(errors(r#"
2015              extend schema
2016                @link(url: "https://specs.apollo.dev/federation/v2.0")
2017                @link(url: "https://custom.dev/f-o.o/v1.0")
2018
2019              type Query {
2020                q: Int
2021              }
2022            "#), @"[]");
2023        }
2024
2025        #[test]
2026        fn errors_for_spec_name_in_schema_that_conflicts_with_past_namespaced_directive() {
2027            insta::assert_debug_snapshot!(errors(r#"
2028              extend schema
2029                @link(
2030                  url: "https://specs.apollo.dev/federation/v2.0"
2031                  import: [{ name: "@key", as: "@foo__key" }]
2032                )
2033                @link(url: "https://custom.dev/foo/v1.0")
2034
2035              type Query {
2036                q: Int
2037              }
2038            "#), @r###"
2039            [
2040                "Cannot import \"@key\" as \"@foo__key\" from feature \"https://specs.apollo.dev/federation\" since it can be confused with a namespaced name from another linked feature \"https://custom.dev/foo\". Please rename the import or feature to avoid conflicts via \"as\".",
2041            ]
2042            "###);
2043        }
2044
2045        #[test]
2046        fn succeeds_renaming_spec_name_that_conflicts_with_past_namespaced_directive() {
2047            insta::assert_debug_snapshot!(errors(r#"
2048              extend schema
2049                @link(
2050                  url: "https://specs.apollo.dev/federation/v2.0"
2051                  import: [{ name: "@key", as: "@foo__key" }]
2052                )
2053                @link(url: "https://custom.dev/foo/v1.0", as: "bar")
2054
2055              type Query {
2056                q: Int
2057              }
2058            "#), @"[]");
2059        }
2060
2061        #[test]
2062        fn errors_for_spec_name_in_schema_that_conflicts_with_future_namespaced_directive() {
2063            insta::assert_debug_snapshot!(errors(r#"
2064              extend schema
2065                @link(url: "https://specs.apollo.dev/federation/v2.0")
2066                @link(
2067                  url: "https://custom.dev/foo/v1.0"
2068                  import: [{ name: "Foo", as: "federation__Foo" }]
2069                )
2070
2071              type Query {
2072                q: Int
2073              }
2074            "#), @r###"
2075            [
2076                "Cannot import \"Foo\" as \"federation__Foo\" from feature \"https://custom.dev/foo\" since it can be confused with a namespaced name from another linked feature \"https://specs.apollo.dev/federation\". Please rename the import or feature to avoid conflicts via \"as\".",
2077            ]
2078            "###);
2079        }
2080
2081        #[test]
2082        fn succeeds_renaming_spec_name_that_conflicts_with_future_namespaced_directive() {
2083            insta::assert_debug_snapshot!(errors(r#"
2084              extend schema
2085                @link(url: "https://specs.apollo.dev/federation/v2.0", as: "bar")
2086                @link(
2087                  url: "https://custom.dev/foo/v1.0"
2088                  import: [{ name: "Foo", as: "federation__Foo" }]
2089                )
2090
2091              type Query {
2092                q: Int
2093              }
2094            "#), @"[]");
2095        }
2096
2097        #[test]
2098        fn errors_for_spec_name_in_schema_that_conflicts_with_past_default_directive() {
2099            insta::assert_debug_snapshot!(errors(r#"
2100              extend schema
2101                @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2102                @link(url: "https://custom.dev/key/v1.0")
2103
2104              type Query {
2105                q: Int
2106              }
2107            "#), @r###"
2108            [
2109                "Cannot import \"@key\" from feature \"https://specs.apollo.dev/federation\" since it can be confused with a namespaced name from another linked feature \"https://custom.dev/key\". Please rename the import or feature to avoid conflicts via \"as\".",
2110            ]
2111            "###);
2112        }
2113
2114        #[test]
2115        fn succeeds_renaming_spec_name_that_conflicts_with_past_default_directive() {
2116            insta::assert_debug_snapshot!(errors(r#"
2117              extend schema
2118                @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2119                @link(url: "https://custom.dev/key/v1.0", as: "foo")
2120
2121              type Query {
2122                q: Int
2123              }
2124            "#), @"[]");
2125        }
2126
2127        #[test]
2128        fn errors_for_spec_name_in_schema_that_conflicts_with_future_default_directive() {
2129            insta::assert_debug_snapshot!(errors(r#"
2130              extend schema
2131                @link(url: "https://specs.apollo.dev/federation/v2.0")
2132                @link(
2133                  url: "https://custom.dev/foo/v1.0"
2134                  import: [{ name: "@foo", as: "@federation" }]
2135                )
2136
2137              type Query {
2138                q: Int
2139              }
2140            "#), @r###"
2141            [
2142                "Cannot import \"@foo\" as \"@federation\" from feature \"https://custom.dev/foo\" since it can be confused with a namespaced name from another linked feature \"https://specs.apollo.dev/federation\". Please rename the import or feature to avoid conflicts via \"as\".",
2143            ]
2144            "###);
2145        }
2146
2147        #[test]
2148        fn succeeds_renaming_spec_name_that_conflicts_with_future_default_directive() {
2149            insta::assert_debug_snapshot!(errors(r#"
2150              extend schema
2151                @link(url: "https://specs.apollo.dev/federation/v2.0", as: "bar")
2152                @link(
2153                  url: "https://custom.dev/foo/v1.0"
2154                  import: [{ name: "@foo", as: "@federation" }]
2155                )
2156
2157              type Query {
2158                q: Int
2159              }
2160            "#), @"[]");
2161        }
2162
2163        #[test]
2164        fn errors_for_spec_name_in_schema_that_conflicts_with_another_spec_name_in_schema() {
2165            insta::assert_debug_snapshot!(errors(r#"
2166              extend schema
2167                @link(url: "https://specs.apollo.dev/federation/v2.0")
2168                @link(url: "https://custom.dev/federation/v1.0")
2169
2170              type Query {
2171                q: Int
2172              }
2173            "#), @r###"
2174            [
2175                "Cannot link feature https://custom.dev/federation as \"federation\" since another feature \"https://specs.apollo.dev/federation\" already uses that alias. Please rename the feature to avoid conflicts via \"as\".",
2176            ]
2177            "###);
2178        }
2179
2180        #[test]
2181        fn succeeds_renaming_spec_name_that_conflicts_with_another_spec_name_in_schema() {
2182            insta::assert_debug_snapshot!(errors(r#"
2183              extend schema
2184                @link(url: "https://specs.apollo.dev/federation/v2.0")
2185                @link(url: "https://custom.dev/federation/v1.0", as: "foo")
2186
2187              type Query {
2188                q: Int
2189              }
2190            "#), @"[]");
2191        }
2192
2193        #[test]
2194        fn errors_for_namespaced_import_that_is_not_a_no_op_import() {
2195            insta::assert_debug_snapshot!(errors(r#"
2196              extend schema
2197                @link(
2198                  url: "https://specs.apollo.dev/federation/v2.0"
2199                  import: [{ name: "@key", as: "@federation__requires" }]
2200                )
2201
2202              type Query {
2203                q: Int
2204              }
2205            "#), @r###"
2206            [
2207                "Cannot import \"@key\" as \"@federation__requires\" from feature \"https://specs.apollo.dev/federation\" since it can be confused with the namespaced name for \"@requires\". Please rename the import to avoid conflicts via \"as\".",
2208            ]
2209            "###);
2210        }
2211
2212        #[test]
2213        fn succeeds_for_namespaced_import_that_is_a_no_op_import() {
2214            insta::assert_debug_snapshot!(errors(r#"
2215              extend schema
2216                @link(
2217                  url: "https://specs.apollo.dev/federation/v2.0"
2218                  import: [{ name: "@key", as: "@federation__key" }]
2219                )
2220
2221              type Query {
2222                q: Int
2223              }
2224            "#), @"[]");
2225        }
2226
2227        #[test]
2228        fn errors_for_default_directive_import_that_is_not_a_no_op_import() {
2229            insta::assert_debug_snapshot!(errors(r#"
2230              extend schema
2231                @link(url: "https://specs.apollo.dev/federation/v2.0")
2232                @link(
2233                  url: "https://custom.dev/foo/v1.0"
2234                  as: "bar"
2235                  import: [{ name: "@baz", as: "@bar" }]
2236                )
2237
2238              type Query {
2239                q: Int
2240              }
2241            "#), @r###"
2242            [
2243                "Cannot import \"@baz\" as \"@bar\" from feature \"https://custom.dev/foo\" since it can be confused with the namespaced name for \"@foo\". Please rename the import to avoid conflicts via \"as\".",
2244            ]
2245            "###);
2246        }
2247
2248        #[test]
2249        fn succeeds_for_default_directive_import_that_is_a_no_op_import() {
2250            insta::assert_debug_snapshot!(errors(r#"
2251              extend schema
2252                @link(url: "https://specs.apollo.dev/federation/v2.0")
2253                @link(
2254                  url: "https://custom.dev/foo/v1.0"
2255                  as: "bar"
2256                  import: [{ name: "@foo", as: "@bar" }]
2257                )
2258
2259              type Query {
2260                q: Int
2261              }
2262            "#), @"[]");
2263        }
2264
2265        #[test]
2266        fn errors_for_imports_of_one_element_to_different_names() {
2267            insta::assert_debug_snapshot!(errors(r#"
2268              extend schema
2269                @link(
2270                  url: "https://specs.apollo.dev/federation/v2.0"
2271                  import: ["@key", { name: "@key", as: "@foo" }]
2272                )
2273
2274              type Query {
2275                q: Int
2276              }
2277            "#), @r###"
2278            [
2279                "Cannot import \"@key\" as \"@foo\" from feature \"https://specs.apollo.dev/federation\" since it was previously imported as \"@key\". Please remove one of these imports.",
2280            ]
2281            "###);
2282        }
2283
2284        #[test]
2285        fn succeeds_for_imports_of_one_element_to_same_name() {
2286            insta::assert_debug_snapshot!(errors(r#"
2287              extend schema
2288                @link(
2289                  url: "https://specs.apollo.dev/federation/v2.0"
2290                  import: [
2291                    { name: "@key", as: "@foo" }
2292                    "@requires"
2293                    { name: "@key", as: "@foo" }
2294                  ]
2295                )
2296
2297              type Query {
2298                q: Int
2299              }
2300            "#), @"[]");
2301        }
2302
2303        #[test]
2304        fn errors_for_import_name_in_schema_that_already_exists_in_different_spec() {
2305            insta::assert_debug_snapshot!(errors(r#"
2306              extend schema
2307                @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2308                @link(
2309                  url: "https://custom.dev/foo/v1.0"
2310                  import: [{ name: "@foo", as: "@key" }]
2311                )
2312
2313              type Query {
2314                q: Int
2315              }
2316            "#), @r###"
2317            [
2318                "Cannot import \"@foo\" as \"@key\" from feature \"https://custom.dev/foo\" since it was previously imported from feature \"https://specs.apollo.dev/federation\". Please rename the import to avoid conflicts via \"as\".",
2319            ]
2320            "###);
2321        }
2322
2323        #[test]
2324        fn succeeds_renaming_import_name_that_already_exists_in_different_spec() {
2325            insta::assert_debug_snapshot!(errors(r#"
2326              extend schema
2327                @link(
2328                  url: "https://specs.apollo.dev/federation/v2.0"
2329                  import: [{ name: "@key", as: "@bar" }]
2330                )
2331                @link(
2332                  url: "https://custom.dev/foo/v1.0"
2333                  import: [{ name: "@foo", as: "@key" }]
2334                )
2335
2336              type Query {
2337                q: Int
2338              }
2339            "#), @"[]");
2340        }
2341
2342        #[test]
2343        fn errors_for_import_name_in_schema_that_already_exists_in_same_spec() {
2344            insta::assert_debug_snapshot!(errors(r#"
2345              extend schema
2346                @link(
2347                  url: "https://specs.apollo.dev/federation/v2.0"
2348                  import: ["@key", { name: "@requires", as: "@key" }]
2349                )
2350
2351              type Query {
2352                q: Int
2353              }
2354            "#), @r###"
2355            [
2356                "Cannot import \"@requires\" as \"@key\" from feature \"https://specs.apollo.dev/federation\" since it was previously imported for \"@key\". Please rename the import to avoid conflicts via \"as\".",
2357            ]
2358            "###);
2359        }
2360
2361        #[test]
2362        fn succeeds_renaming_import_name_that_already_exists_in_same_spec() {
2363            insta::assert_debug_snapshot!(errors(r#"
2364              extend schema
2365                @link(
2366                  url: "https://specs.apollo.dev/federation/v2.0"
2367                  import: [
2368                    { name: "@key", as: "@requires" }
2369                    { name: "@requires", as: "@key" }
2370                  ]
2371                )
2372
2373              type Query {
2374                q: Int
2375              }
2376            "#), @"[]");
2377        }
2378
2379        #[test]
2380        fn errors_for_used_shadowed_directive_import() {
2381            insta::assert_debug_snapshot!(errors(r#"
2382              extend schema
2383                @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2384
2385              directive @federation__key(
2386                fields: federation__FieldSet!
2387                resolvable: Boolean = true
2388              ) repeatable on OBJECT | INTERFACE
2389
2390              scalar federation__FieldSet
2391
2392              type Query {
2393                users: [User!]!
2394              }
2395
2396              type User @federation__key(fields: "id") {
2397                id: ID!
2398                name: String!
2399              }
2400            "#), @r###"
2401            [
2402                "Cannot import \"@key\" from feature \"https://specs.apollo.dev/federation\" since there's a used definition for the namespaced name \"@federation__key\". Please switch usages of the namespaced name to the import name and remove the definition.",
2403            ]
2404            "###);
2405        }
2406
2407        #[test]
2408        fn succeeds_for_unused_shadowed_directive_import() {
2409            insta::assert_debug_snapshot!(errors(r#"
2410              extend schema
2411                @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])
2412
2413              directive @federation__key(
2414                fields: federation__FieldSet!
2415                resolvable: Boolean = true
2416              ) repeatable on OBJECT | INTERFACE
2417
2418              scalar federation__FieldSet
2419
2420              type Query {
2421                users: [User!]!
2422              }
2423
2424              type User @key(fields: "id") {
2425                id: ID!
2426                name: String!
2427              }
2428            "#), @"[]");
2429        }
2430
2431        #[test]
2432        fn errors_for_used_shadowed_type_import() {
2433            insta::assert_debug_snapshot!(errors(r#"
2434              extend schema
2435                @link(
2436                  url: "https://specs.apollo.dev/federation/v2.0"
2437                  import: ["FieldSet"]
2438                )
2439
2440              scalar federation__FieldSet
2441
2442              type Query {
2443                users: [User!]!
2444              }
2445
2446              type User {
2447                id: ID!
2448                fieldSet: federation__FieldSet!
2449              }
2450            "#), @r###"
2451            [
2452                "Cannot import \"FieldSet\" from feature \"https://specs.apollo.dev/federation\" since there's a used definition for the namespaced name \"federation__FieldSet\". Please switch usages of the namespaced name to the import name and remove the definition.",
2453            ]
2454            "###);
2455        }
2456
2457        #[test]
2458        fn succeeds_for_unused_shadowed_type_import() {
2459            insta::assert_debug_snapshot!(errors(r#"
2460              extend schema
2461                @link(
2462                  url: "https://specs.apollo.dev/federation/v2.0"
2463                  import: ["FieldSet"]
2464                )
2465
2466              scalar federation__FieldSet
2467
2468              type Query {
2469                users: [User!]!
2470              }
2471
2472              type User {
2473                id: ID!
2474                fieldSet: String!
2475              }
2476            "#), @"[]");
2477        }
2478
2479        #[test]
2480        fn succeeds_for_shadowed_type_import_used_in_shadowed_import() {
2481            insta::assert_debug_snapshot!(errors(r#"
2482              extend schema
2483                @link(
2484                  url: "https://specs.apollo.dev/federation/v2.0"
2485                  import: ["@key", "FieldSet"]
2486                )
2487
2488              directive @federation__key(
2489                fields: federation__FieldSet!
2490                resolvable: Boolean = true
2491              ) repeatable on OBJECT | INTERFACE
2492
2493              scalar federation__FieldSet
2494
2495              type Query {
2496                users: [User!]!
2497              }
2498
2499              type User {
2500                id: ID!
2501                fieldSet: String!
2502              }
2503            "#), @"[]");
2504        }
2505    }
2506
2507    #[test]
2508    fn allowed_link_directive_definitions() -> Result<(), FederationError> {
2509        let link_defs = [
2510            "directive @link(url: String!, as: String) repeatable on SCHEMA",
2511            "directive @link(url: String, as: String) repeatable on SCHEMA",
2512            "directive @link(url: String!) repeatable on SCHEMA",
2513            "directive @link(url: String) repeatable on SCHEMA",
2514        ];
2515        let schema_prefix = r#"
2516          extend schema @link(url: "https://specs.apollo.dev/link/v1.0")
2517          type Query { x: Int }
2518        "#;
2519        for link_def in link_defs {
2520            let schema_doc = format!("{schema_prefix}\n{link_def}");
2521            let schema = Schema::parse(&schema_doc, "test.graphql").unwrap();
2522            let meta = LinksMetadata::from_schema(&schema)?;
2523            assert!(meta.is_some(), "should have metadata for: {link_def}");
2524        }
2525        Ok(())
2526    }
2527}