Skip to content

fix: update visitor logic in topologicalSortAST to fix Issue #528 #958

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

Conversation

trevor-anderson
Copy link
Contributor

Currently, there is a problem described in Issue #528 whereby "const" output can have some variables used before they're defined. There were two related issues in the visit invocation in topologicalSortAST that caused the problem. These changes resolve "use before defined" errors in generated output and enhance compatibility with custom scalars and circular type definitions.

  • Recursively unwrap GraphQL types (List/NonNull) via getNamedType to correctly register dependencies.
  • Add INTERFACE_TYPE_DEFINITION to the list of target kinds for proper dependency handling.

Additionally, this commit updates the topsort algo to iterate over all nodes (g.nodes().forEach(visit)) instead of just sinks, ensuring self-referential and cyclic dependencies (e.g. self-circular inputs) are processed.

Since these changes affect output, impacted tests have had their expect-values updated as well (just re-ordering of certain type declarations).

…ex#528

Currently, there is a problem described in Issue Code-Hex#528 whereby "const"
output can have some variables used before they're defined. There were
two related issues in the `visit` invocation in `topologicalSortAST`
that caused the problem. These changes resolve "use before defined"
errors in generated output and enhance compatibility with custom scalars
and circular type definitions.

- Recursively unwrap GraphQL types (List/NonNull) via `getNamedType` to
correctly register dependencies.
- Add `INTERFACE_TYPE_DEFINITION` to the list of target kinds for proper
dependency handling.

Additionally, this commit updates the `topsort` algo to iterate over all
nodes (`g.nodes().forEach(visit)`) instead of just sinks, ensuring
self-referential and cyclic dependencies (e.g. self-circular inputs) are
processed.

Since these changes affect output, impacted tests have had their
expect-values updated as well (just re-ordering of certain type
declarations).
Copy link
Owner

@Code-Hex Code-Hex left a comment

Choose a reason for hiding this comment

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

Thank you! I've made a few comments.

@@ -235,13 +235,13 @@ describe('topologicalSortAST', () => {
const sortedSchema = getSortedSchema(schema);

const expectedSortedSchema = dedent/* GraphQL */`
input A {
Copy link
Owner

Choose a reason for hiding this comment

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

Why do I need to change this test case? I expect this test case to pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The relevant code change is in the topsort algo fn, where g.sinks().forEach(visit) is replaced with g.nodes().forEach(visit) — that change makes this necessary.

The reason for the change is that, in order to ensure that every node is visited, we need to iterate over all nodes in the graph. In the presence of cycles, some nodes may not be considered sinks (i.e., they have no outgoing edges), and therefore would be skipped in a traditional topological sort. This is precisely what was happening in this test case with the old implementation — it's why input A and input B were not re-ordered like all the other test cases.

The new g.nodes() approach guarantees that every definition is included. The difference is in the starting point for the DFS that produces the order:

  • Using g.sinks().forEach(visit):
    This approach starts the DFS only from nodes that are sinks — that is, nodes with no outgoing edges that do not depend on any other nodes. In a graph without cycles, sinks are safe starting points because they can be placed first in the order. However, if a node is part of a cycle — or self-references, as in the relevant test-case — it will have an outgoing edge (even if that edge is to itself), so it will not be considered a sink. That means the DFS might never start from those nodes, and they could be entirely skipped in the ordering. This is why input A and input B were not re-ordered in the previous implementation.

  • Using g.nodes().forEach(visit):
    This approach goes over every node in the graph and ensures that each one is visited, regardless of whether it’s a sink or not. This is especially important for self-circular or mutually dependent nodes, because even if they are in a cycle (and therefore not sinks), they’ll still be processed and appear in the final list.

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for your explanation!
Let's Go!!

case 'InputObjectTypeDefinition': {
case Kind.OBJECT_TYPE_DEFINITION:
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
case Kind.INTERFACE_TYPE_DEFINITION: {
Copy link
Owner

Choose a reason for hiding this comment

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

This is a new case, so please add a test to confirm that the interface is also sorted

Copy link
Owner

@Code-Hex Code-Hex left a comment

Choose a reason for hiding this comment

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

Thank you for your support!

@Code-Hex Code-Hex merged commit 75378cc into Code-Hex:main Feb 9, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants