-
Notifications
You must be signed in to change notification settings - Fork 117
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
Clarify ConstNodeInspector code #39
Conversation
cd73672
to
d24769d
Compare
require "packwerk/node" | ||
|
||
module Packwerk | ||
# Extracts the implicit constant reference from an active record association | ||
class AssociationInspector | ||
include ConstantNameInspector |
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.
I found it really confusing that the interfaces aren't postfixed with interface and made it hard to understand the code initially.
namespace_path = Node.enclosing_namespace_path(node, ancestors: ancestors) | ||
constant_name = Node.constant_name(node) | ||
namespace_path.push(constant_name).join("::") | ||
end |
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.
Some of these private methods could live in another class, but tackling that issue in this PR would increase the scope of the PR too much and make it harder to review.
lib/packwerk/reference_extractor.rb
Outdated
@@ -17,7 +17,7 @@ class ReferenceExtractor | |||
sig do | |||
params( | |||
context_provider: Packwerk::ConstantDiscovery, | |||
constant_name_inspectors: T::Array[Packwerk::ConstantNameInspector], |
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.
When I did my first pass through the code, not having the interface postfix for this constant made the code harder to understand. So I'm adding one here.
Ditto my comment on #42 about commits. Also, do we normally use an |
I don't think a convention exists, at least at Shopify, and I find it very confusing. IMO, we should have a different method to include it ( The problem is not in the interface definition; it's obvious from the first few lines of the modules that it's an interface. It's in implementing the interface. So having a more specific method to do that would be better than an |
@exterm We don't have a company convention around this, but in my previous team, it was a team convention. My guiding principle here is that we want to write our code that is easy to understand. When reading the code that includes an interface, it is not clear what is happening. We could either include methods from a module or implement an interface. Granted that this is a minor inconvenience, those add up and contribute to code that is hard to follow. Having a dedicated keyword would be the best solution, but that doesn't strike me as the most practical solution here. I could try to add it to sorbet if this is something we desire. @Morriar Do you think this would be a good addition?
Yes, we don't suffix Classes nor Modules, but those constructs are first-class citizens in Ruby. Interfaces are not. We are following similar conventions for constructs like Controllers, Services, and Views. The only exceptions are Models, which is possible because every other class is suffixed in rails and the models live in a dedicated model folder. |
8410e30
to
30ed5b1
Compare
I opened an issue for the interface issue on sorbet sorbet/sorbet#3578. For the time being, I'm going to drop the changes to the interface in the PR. I also split the commit into multiple commits as @tomstuart suggested. |
30ed5b1
to
23d4c49
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.
Thank you for doing this @doodzik! It’s a positive change and I’m glad to see the code becoming clearer.
In a few places I’ve added some historical context which I hope helps you to decide whether you want to commit to these changes. It’s all just food for thought; let me know what you think and I’ll come back and approve if you’re happy.
@@ -14,7 +14,6 @@ | |||
require "packwerk/configuration" | |||
require "packwerk/const_node_inspector" | |||
require "packwerk/constant_discovery" | |||
require "packwerk/constant_name_inspector" |
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.
I can’t decide whether this require
is unnecessary because I don’t really know what any of the require
s in lib/packwerk.rb
are for. Is the intention for it to require
everything in the “public API” of Packwerk so that anyone using the gem can refer to e.g. Packwerk::Inflections
without needing to require
it themselves? If so, maybe lots of these require
s are unnecessary because they’re loading Packwerk internals; but conversely, if the goal is to have a single file which loads every class & module in Packwerk to ensure everything is available, it seems simplest to exhaustively list them all here rather than try to keep track of which ones recursively require which other ones (since that can change over time).
@exterm, do you have any context on the intention here?
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.
I prefer requiring a file where it is actually used because it makes the dependencies clearer. In this case, the requires
in lib/package.rb
would only contain the entry points into the application and what we intend to expose to our users. This also means that you will not get into a situation where you don't know what any of the requires
is for.
I think there is an argument for convenience by requiring every file in the lib/package.rb
file, but I think having clearly defined requires
makes the codebase easier to understand in the long run.
I would like us to align one approach since this allows us to be consistent in the codebase, but I'm happy to go with either approach. So thank you for raising this issue :)
@@ -9,12 +9,12 @@ class ConstNodeInspector | |||
include ConstantNameInspector | |||
|
|||
def constant_name_from_node(node, ancestors:) | |||
return nil unless Node.type(node) == Node::CONSTANT | |||
return nil unless Node.constant?(node) |
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.
There’s a fair amount of discussion about this design decision on the (Shopify-only) Shopify/packwerk-old#266 PR. The idea is that there were many places in Packwerk where we wanted to test for multiple node types, e.g. “if this is a class or module or constant assignment, then …, otherwise …”, so the API was designed to be usable in a case
statement:
case Node.type(node)
when Node::CLASS, Node::MODULE, Node::CONSTANT_ASSIGNMENT
# …
when Node::BLOCK
# …
else
# …
end
If we provide predicates instead then this becomes a slightly less convenient if Node.class?(node) || Node.module?(node) || Node.constant_assignment?(node)
. I was reluctant to provide both because that would mean our Packwerk::Node
API offers two different ways of doing the same thing, which makes it (again slightly) less straightforward to use.
Looking at the code now, I can see that essentially all of these case
statements have been moved into Packwerk::Node
itself, so perhaps there’s a case for making Node.type
private and exposing only predicates to the rest of Packwerk. WDYT?
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.
API offers two different ways of doing the same thing
Agreed, I think it makes sense to keep the API as straightforward to use as possible.
Looking at the code now, I can see that essentially all of these case statements have been moved into
Packwerk::Node
itself, so perhaps there’s a case for making Node.type private and exposing only predicates to the rest of Packwerk.
I did a quick experiment to verify that we use it only in Packwerk::Node
, and we do. So it shouldn't be an issue to change Node.type
to be private.
Looking at the Node Module and how it uses the switch statements, it seems to me that we are missing a Factory here. I would suggest a separate PR to move as much logic out of the Node module into dedicated modules, converting the Node module into a Factory module. And then, as you mentioned, change the Node.type
method to be private. This would clean up the code and simplify the API of Packwerk::Node
WDYT?
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.
Looking at the Node Module and how it uses the switch statements, it seems to me that we are missing a Factory here. I would suggest a separate PR to move as much logic out of the Node module into dedicated modules, converting the Node module into a Factory module.
When we discussed this on a call, I think we agreed that a) it’s a good idea but b) you’re not going to do it at the moment.
@@ -40,5 +35,19 @@ def constant_in_module_or_class_definition?(node, parent:) | |||
parent_name && parent_name == Node.constant_name(node) | |||
end | |||
end | |||
|
|||
def fully_qualify_constant(node, ancestors:) |
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.
Extracting these private methods is a good idea. I would love to suggest better names for them to more clearly describe what they’re doing, but I actually don’t understand what the code does in the first place (why is it sometimes returning a “parent module name” generated only from the ancestor nodes, but sometimes a ”qualified constant” name generated from the node itself?) so can’t begin to think of anything better. Fixing the deeper problem is a job for another PR. 😄
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.
Agreed, I'm not confident in why the code does what it does. I think it would be good to go through the test cases and document how the data gets manipulated in those methods. I would add to this that we can probably extract those private methods into an Ancestor module in a separate PR and investigate and clarify how it works there. WDYT?
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.
Again, it’s a good idea — let’s decide separately whether it’s worth doing in another PR.
lib/packwerk/const_node_inspector.rb
Outdated
# Only process the root `const` node for namespaced constant references. For example, in the | ||
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`. | ||
parent = ancestors.first | ||
return nil if parent && Node.constant?(parent) | ||
return nil if root_constant?(ancestors) |
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.
The name root_constant?
is the opposite of what this check is doing — as the comment says, we want to ignore every node except Spam
in Spam::Eggs::Thing
, so we return nil
if we find ourselves trying to process a node like Eggs
or Thing
whose parent is another constant. If you want to keep the name root_constant?
then the conditional should be unless root_constant?
and the behaviour of the predicate should be reversed.
lib/packwerk/const_node_inspector.rb
Outdated
|
||
if constant_in_module_or_class_definition?(node, parent: parent) | ||
if constant_in_module_or_class_definition?(node, ancestors: ancestors) |
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.
I don’t think this part of the change is worth it, because we’re now intentionally passing more information into constant_in_module_or_class_definition?
than it needs to do its job. For the caller it’s useful to be able to see at a glance that this decision depends only upon the parent node, not the whole ancestor chain, and for the implementer it’s helpful to be prevented from accidentally poking around in the rest of the ancestor chain unless you really mean it (and deliberately change the arguments to reflect that). WDYT?
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.
Agreed, I'm providing too much in the private method and making the intention behind the code harder to understand 👍
lib/packwerk/const_node_inspector.rb
Outdated
end | ||
rescue Node::TypeError |
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.
I’m on the fence about this one. The rescue
was originally added to deal with an occurrence of self.class::HEADERS
in core. I agree that Node.constant_name
can raise TypeError
elsewhere in this method but that’s not something we’ve seen happen so I’m not able to decide whether returning nil
would be the right thing to do in those unknown cases.
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.
I'm going to remove this change from the PR because another PR was opened that deals with this issue more concretely ref #54
I'm removing the require statement for ConstantNameInspector in the main packwerk file. The class is require at the file that uses it and doesn't need to be specified a second time.
I'm adding a `.constant?` class method to the Node class to check if a node instance is a constant or not. Before this change we would use `Node.type(node) == Node::CONSTANT` to check if a node is a constant. This however exposes too many internals and can be abstracted into a class method, which makes the code easier to understand.
I'm moving a chunk of code out of the constant_name_from_node method. This makes the intend clearer.
23d4c49
to
71cee51
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.
Looks good. Thanks for your patience! 🙏
lib/packwerk/const_node_inspector.rb
Outdated
# Only process the root `const` node for namespaced constant references. For example, in the | ||
# reference `Spam::Eggs::Thing`, we only process the const node associated with `Spam`. | ||
def root_constant?(parent) | ||
!parent || !Node.constant?(parent) |
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.
For me this would be 1% easier to understand as !(parent && Node.constant?(parent))
(per de Morgan) but there’s not much in it.
I agree with the answer given by the Sorbet team on this: it would be hard to enforce using the static checker for retro-compatibility purposes so we would end up with the two forms in our codebase which would be super confusing for the developers. |
71cee51
to
ea2e68a
Compare
Add pronounciation to README
What are you trying to accomplish?
I was going through the code to implement #15 and I had a hard time following the code in the ConstNodeInspector class. As such I refactored it to be a bit more self-explanatory.
Type of Change
Checklist