Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor removeDirectivesFromDocument to fix AST ordering sensitivities and avoid 1/3 AST traversals #10560

Merged
merged 20 commits into from
Feb 15, 2023

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Feb 15, 2023

This refactoring of removeDirectivesFromDocument builds on (rebases) the work begun in PR #6322, fixing both the original issue #6311 and the related issue #10539.

At a high level, these changes reduce the number of full-document AST traversals performed by removeDirectivesFromDocument from 3 to 2. Future work could potentially replace the second traversal with a more targeted manual cleanup pass, to improve performance further.

In the first of those two traversals, we gather as much information as we can about the AST, including all the variable references and fragments spreads used by each operation or fragment definition. At the same time, we can safely remove subtrees annotated with removal directives like @client, which effectively hides those removed subtrees from the information collected, and from future traversals.

For fragment definitions modified by these removals, if they are left with only a __typename field, we proactively remove the whole fragment definition along with any ...spread references to it. However, if a fragment definition contains only a __typename field but is not otherwise modified, that fragment definition remains unmodified in the AST (preserving the spirit of #6311), unless there are no ...spread references to it, in which case it must be removed, like any other unused fragment definition.

An additional exception is made for documents that contain only fragment definitions, with no operations consuming them: removing these "unused" fragments seems wrong, so the removal of unused and/or __typename-only fragment definitions is disabled when there are no operations in the document, on the presumption these "unused" fragment definitions must have a use beyond this document (for example, selecting a specific fragment from a document containing several definitions using useFragment's options.fragmentName).

Using the information obtained in the first visit, a second visit brings all fragment spreads and definitions into agreement with each other, existence-wise, so there can be no dangling spreads or unused definitions in the final document. This pass also prunes variable declarations from operations that no longer use some of their declared variables, including usages directly within the operation and in any transitively included fragments.

I've added several more tests to enforce more edge cases around ordering sensitivity and transitive variable/fragment usage. I believe this PR has roughly the same level of backwards compatibility as the previous PR #6322, though of course the changes are more significant now, so we might want to add even more tests.

@benjamn benjamn self-assigned this Feb 15, 2023
@benjamn benjamn requested a review from alessbell February 15, 2023 16:07
@changeset-bot
Copy link

changeset-bot bot commented Feb 15, 2023

🦋 Changeset detected

Latest commit: 251a128

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@apollo/client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment on lines -429 to -431
function getAllFragmentSpreadsFromSelectionSet(
selectionSet: SelectionSetNode,
): FragmentSpreadNode[] {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper function is no longer needed because we no longer have to traverse subtrees removed by @client directives to find the fragment spreads they contain. Instead, we simply fail to record fragments spreads within removed subtrees, and later remove any/all unused fragments.

@alessbell
Copy link
Contributor

Looks like this PR also closes #5192 #10003 🎉

return getInUseByFragmentName(ancestor.name.value);
}
}
invariant.error(`Could not find operation or fragment`);
Copy link
Contributor

@alessbell alessbell Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test for the error case?

Edit: it seems it would always be a GraphQL syntax error? In which case I see it wouldn't be necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a reason this error should ever happen (assuming well formed GraphQL input), but I'm a little hesitant to throw here.

Copy link
Contributor

@alessbell alessbell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An all around improvement that resolves several issues while also reducing AST traversals 🎉🎉🎉

Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had some minor feedback on this one. Typically I use [nit] comments to point out things that I wouldn't block the PR over, so use your own discretion on whether you'd like to apply those changes or not.

src/utilities/graphql/transform.ts Outdated Show resolved Hide resolved
src/utilities/graphql/transform.ts Outdated Show resolved Hide resolved
src/utilities/graphql/transform.ts Outdated Show resolved Hide resolved
src/utilities/graphql/transform.ts Outdated Show resolved Hide resolved
@benjamn benjamn force-pushed the rebase-pr-6322-to-fix-issue-6311 branch 2 times, most recently from d5471bf to fc20585 Compare February 15, 2023 21:59
@benjamn benjamn force-pushed the rebase-pr-6322-to-fix-issue-6311 branch from fc20585 to ced0bd0 Compare February 15, 2023 22:14
mtsmfm and others added 6 commits February 15, 2023 17:25
Consider the following schema on the server:

```graphql
interface Character {
  appearsIn: [Episode]!
}

type Human implements Character {
  appearsIn: [Episode]!
  height: Int
}
```

Then the following addition on the client:

```graphql
type Droid implements Character {
  appearsIn: [Episode]!
  primaryFunction: String
}
```

With the following query:

```graphql
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    ... on Human {
      height
    }
    ... on Droid @client {
      primaryFunction
    }
  }
}
```

The server will complain that the fragment spread has no type overlap
with parent, as the server does not have knowledge of the `Droid` type.

This commit strips out inline fragments which use a client directive,
allowing them to be resolved via local state.
@benjamn benjamn force-pushed the rebase-pr-6322-to-fix-issue-6311 branch from ced0bd0 to 1a2bd3d Compare February 15, 2023 22:25
@benjamn benjamn merged commit dc17d11 into main Feb 15, 2023
This was referenced Feb 15, 2023
@jerelmiller jerelmiller deleted the rebase-pr-6322-to-fix-issue-6311 branch February 15, 2023 22:51
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 18, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.