Skip to content

Avoid returning nil values from ClassDeclaration#child_nodes #16

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

nvasilevski
Copy link

For a class declaration node having superclass is optional so currently ClassDeclaration#child_nodes method returns an array that contains a nil value which complicates traversing a little bit.
Here is an example:

SyntaxTree.parse("class A;end").child_nodes.first.body.first.child_nodes.map(&:class)
 => [SyntaxTree::ConstRef, NilClass, SyntaxTree::BodyStmt] 

I wasn't sure about our intentions, but I feel like it might be helpful to expect child_nodes to always return an array of node objects and nothing else so I opened this PR as a discussion with a potential solution.

The solution I'm suggesting may look excessive but I end up choosing it versus calling .compact on the resulting array because I liked that we explicitly state which part of the class declaration is optional. Let me know if you have other suggestions!

After the change:

3.0.3 :014 > SyntaxTree.parse("class A;end").child_nodes.first.body.first.child_nodes.map(&:class)
 => [SyntaxTree::ConstRef, SyntaxTree::BodyStmt] 

If we think that this is a reasonable change to implement, I can quickly change similar nodes like BodyStmt, for example:
https://github.com/kddnewton/syntax_tree/blob/d738840feab02648899f08d00dc9fcf5f018f6a3/lib/syntax_tree.rb#L2850

I haven't covered it with tests because wasn't sure if that would be a correct change to do so let me know if I should add some.

Thanks!

@kddnewton
Copy link
Member

Hmm... I'm really hesitant on this tbh. The snippet you provided:

SyntaxTree.parse("class A;end").child_nodes.first.body.first.child_nodes.map(&:class)

explicitly depends on array indices (the 2 calls to #first). Dynamically changing the size of the result of child_nodes based on its content means there's no guarantee of the same shape. So you couldn't, for instance, run:

name, superclass, bodystmt = class_node.child_nodes

Because now superclass could end up being the bodystmt if the superclass didn't exist.

I get where you're coming from about traversing though. Maybe it would be better to be explicit about which nodes you're pulling out though. I recently added support for pattern matching to the nodes (I still need to push up the code for the remaining nodes), but you could use that to your advantage here:

SyntaxTree.parse("class A; end") => { statements: { body: [{ constant:, superclass:, bodystmt: }] } }

Because now you have constant, superclass, and bodystmt as local variables that you can use. What do you think?

@nvasilevski
Copy link
Author

means there's no guarantee of the same shape

Ah, so that's was basically my main question. Whether or not we expect child_nodes to return an array of a certain shape or just an array of existing child nodes

name, superclass, bodystmt = class_node.child_nodes

Thats a good example 👍 I guess in some places I'd go with
name, superclass, bodystmt = class_node.name, child_node.superclass, child_node.bodystmt

to be a little bit more explicit and to avoid depending on a certain child_nodes implementation/shape

But the general intention makes sense to me 👍 Thanks for clarifying!
I am definitely going to check out pattern matching 👀

@nvasilevski nvasilevski deleted the optional-superclass-in-class-declaration branch January 31, 2022 15:02
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