-
Notifications
You must be signed in to change notification settings - Fork 2k
[WIP] Refactor 'visit' function #1145
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
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.
Thanks for investigating this. I'd prefer if this were broken up into a few smaller PRs since this is a bit difficult to follow. We'll probably need to add more tests against the visitor before refactoring.
Since the issue is understandability of the API - perhaps it's best to start with expanding the documentation above the function and the corresponding type definitions. The implementation should not need to change much if at all.
Later, we can investigate PRs with more focused implementation refactors
@@ -538,3 +538,56 @@ export type DirectiveDefinitionNode = { | |||
+arguments?: $ReadOnlyArray<InputValueDefinitionNode>, | |||
+locations: $ReadOnlyArray<NameNode>, | |||
}; | |||
|
|||
export type KindToASTNodeType = {| |
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.
It would be nicer to instead use $PropertyType<T, 'kind'>
instead of manually assembling this type
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.
My goal was to provide correct types for node
argument of callback depending on how it registered, e.g.:
visit(ast, {
Document(node) {
// node should be DocumentNode and not ASTNode
}
});
The only way to do this is to create the type like that:
import type {
DocumentNode,
// ...
} from './ast';
type Visitor = {
enter: VisitorFn<ASTNode> | {
Document: VisitorFn<DocumentNode>,
...
},
Document: VisitorFn<DocumentNode> | { enter?: VisitorFn<DocumentNode>, leave?: VisitorFn<DocumentNode>}
// ...
}
So I need to list every node type three times. That's why I tried to reduce it to the following:
import type { KindToASTNodeType } from './ast';
type KindVisitor = {|
...$ObjMap<KindToASTNodeType, <T>(T) => VisitorFn<T>>,
|};
type Visitor = {|
enter?: VisitorFn<ASTNode> | KindVisitor,
leave?: VisitorFn<ASTNode> | KindVisitor,
...$ObjMapi<KindToASTNodeType , <T>(T) => NodeVisitor<T>>,
|};
I really like to properly type visitor
argument because without it you can achieve type-safety inside visitor callbacks.
I'm new to flow, so can you please give example how I can achieve the same result with $PropertyType<T, 'kind'>
?
Or should I just list every node type without trying to use clever tricks?
Yes,
Let's focus on changes to the API in separate focused PRs. We have code that relies on this behavior so we would need to include a migration plan - I'm not sure that's something we want to do.
There is existing code which relies on this - we could change it in a focused PR with a clear migration plan, but we would need to weigh the benefits with costs. It's not immediately clear to me that it would be worth it.
There are use cases where you need to access sibling items in an array and being able to walk up the ancestors path is used to do so. Again - there may be a subjectively improved API out there, but it comes at the cost of building a migration strategy for existing tools. We should focus on API changes holistically and separate from any refactoring and type/documentation changes |
Agree 👍 Also, I would love to enable Flow in this file and add typings in separate PR. Actually, I tried to do this initially but was overwhelmed by a number of flow errors which is impossible to fix without code changes. |
If I were tackling this, I'd probably split this process up. First, I'd create the "common API", well-typed functions. For instance, maybe having one function for calling Once we have well-typed wrapping functions for each specific (and hopefully relatively generic) use case that we think is important, we could then work on migrating existing calls and cleaning up the inner workings of |
Unfortunately I couldn't figure it out either. I think your approach of explicitly listing a mapping of kind to node type works pretty well (ideally, I would have flow infer it from the |
const visitFn = getVisitFn(visitor, node.kind, isLeaving); | ||
|
||
if (visitFn) { | ||
const key = path[path.length - 1]; |
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.
create variable key
before line 216 and use it in 216 line
const isInArray = typeof key === 'number';
and in 218 line
const curIndex = isInArray ? key : undefined;
@IvanGoncharov do you still want to make this refactor? I know there's been a bit of churn in the visitor function, not sure if this is still relevant. |
Converting into #3225 issue |
I very frequently use
visit
function in my work. However I never fully understood how it's working especially since entirevisitor.js
lacks typing.I managed to figure out some things and add typings to entire file (with the exception of a few
any
) and all tests are passing. But I stuck since I can't understand original intentions for some of the features:@leebyron Could you please review this PR and also answer a few questions:
visit
was designed to handle arrays inroot
as an argument, but this feature is not used anywhere. Is it required functionality?parent
of visitor callback is very strange. If the current node is not in an array it has a value of parent node as expected but if it's inside an array thenparent
is undefined. In this PR I madeparent
to always point to parent ASTNode as I think it's what's expected. Is it possible to merge such change?key
parameter is always equal to the last element ofpath
array and can be either number or string depending on node location. I see that absolute majority of callback ignores this parameter, so maybe it's worth removing it (you always can get the same value from the path) to simplify callback signature?ancestors
is an array of both ASTNodes and arrays of ASTNodes which is very confusing + you can get the same array from root usingpath
elements as indexes. In my PR I changedancestors
to be an array of parent ASTNodes. Is it possible to merge such change?I saw you made a few small breaking changes in the recent PRs. I think it would be great to also include some interface cleanup of
visit
function. Since at the moment it scares away 3rd-party developers from writing tooling or contributing to existing projects. For example here is an issue reported on GraphQL Faker repository: