-
Notifications
You must be signed in to change notification settings - Fork 2k
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
fix(validation): catch OverlappingFieldsCanBeMergedRule violations with nested fragments #4168
fix(validation): catch OverlappingFieldsCanBeMergedRule violations with nested fragments #4168
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great find, just letting you know it's on my radar, will review it more in depth in the following days
Hi @sachindshinde, I'm @github-actions bot happy to help you with this PR 👋 Supported commandsPlease post this commands in separate comments and only one per comment:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes a lot of sense to me CC @graphql/graphql-js-reviewers for the second review, this seems like a legit bug though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that this is a great find and fix!
Only feedback is whether we can perhaps use a separate compared<*>Map
for when comparing fields and fragments, as otherwise the naming gets confusing.
I took a stab at how that might look: sachindshinde/graphql-js@fix-16.3.0-OverlappingFieldsCanBeMergedRule...yaacovCR:suggestions-for-overlapping-fix
We could always do that in a separate PR.
If we do choose to add a separate map, it might make sense to also add a separate ruleContext
that bundles all of these maps, which should probably be a separate PR which we can do later: yaacovCR/graphql-js@suggestions-for-overlapping-fix...rule-context
As an aside, applying this PR (and my first suggestion) onto main, we get the following results for our validation benchmarks as compared to main: (note that the base of this PR is 16.x.x but we will of course need this on main as well -- again, great pickup @sachindshinde !!!!) |
Thanks for the reviews! @yaacovCR
I've pushed another commit to this PR (the seventh commit), that addresses the above two issues via Typescript function overloading and by providing a comparator for |
9e5df54
to
43bf1e7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great find, great fix, amazing TS wizardry!
Looking at this again => it seems that our pairs in We could introduce an Let me know what you think? Thanks again for finding this! |
here is the benchmark, shows an essential equivalence, but I would still favor that approach, I think it incidentally makes clearer what's going on in |
… compareFragmentPairs usage
…ents when recursing into fragments (but not when recursing into fields, to save memory)
…) in light of bug
…agment, and modify collectConflictsBetweenFieldsAndFragment() to memoize the selection set and fragment
Co-authored-by: Yaacov Rydzinski <[email protected]>
we do not presently need it
c2f0e5e
to
dd7d0e6
Compare
…th nested fragments (graphql#4168) Port of graphql#4168 to v17
@sachindshinde -- I took the liberty of pushing my suggestion and merging, if you would like to make additional changes, feel free to open a PR and fix-up as you would like. Thanks again so much for finding this!! |
In trying to prevent infinite loops, #3442 introduced a bug that causes certain violations of the Field Selection Merging validation to not be caught (released in
16.3.0
, and backported to15.9.0
). This PR fixes this bug, while continuing to prevent infinite loops.Specifically, the code introduced in that PR misuses
comparedFragmentPairs
in thecollectConflictsBetweenFieldsAndFragment()
function. That function takes in fields/a selection set and checks whether it conflicts with the given fragment, while recursively checking those fields against nested fragments in the given fragment. ThecomparedFragmentPairs
data structure is meant to store whether fragments have been checked for conflict against other fragments, but thecollectConflictsBetweenFieldsAndFragment()
does not compare fragments against nested fragments (only the selection set against fragments).For an example of how this causes violations to be missed, see the test in the first commit. The first fragment causes an entry to be added to
comparedFragmentPairs
for the pair of the second and third fragments. When the query's selection set introduces a field that conflicts with a field in the third fragment,collectConflictsBetweenFieldsAndFragment()
will then erroneously skip the third fragment due to the presence incomparedFragmentPairs
. The second commit accordingly reverses the non-test changes of #3442.The third commit shows a naive approach to preventing infinite loops, where we keep track of fragments seen while recursing into nested fragments in
collectConflictsBetweenFieldsAndFragment()
. This approach notably doesn't track the set of seen fragments across field boundaries, which is less memory overhead. However this unfortunately introduces a separate bug, shown by the test in the fourth commit. This test creates nested fragments across field boundaries in such a way as to cause an infinite loop (collectConflictsBetweenFieldsAndFragment()
eventually calls itself with the same arguments when it recurses into fields). The fifth commit accordingly reverses these changes.The sixth commit introduces an actual fix. The
comparedFragmentPairs
data structure has been updated to additionally store a pair of fragment and fields/selection set instead of just two fragments. We then memoize comparisons between fields/fragments at the start of thecollectConflictsBetweenFieldsAndFragment()
function (similar to the fragment pair memoization at the start ofcollectConflictsBetweenFragments()
). Note that this will increase the size ofcomparedFragmentPairs
to scale with the product of the number of selection sets and the number of fragments, whereas previously it scaled with the square of the number of fragments (though I'm unsure if this extra overhead is significant in the context of full GraphQL validation).