-
Notifications
You must be signed in to change notification settings - Fork 113
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
Extract CLI commands to individual classes #351
Conversation
098c4d6
to
a83f2bd
Compare
autoload_under "commands" do | ||
autoload :OffenseProgressMarker | ||
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.
This does not seem necessary, and was actually interfering with the autoload :Commands
that we added above.
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.
👍
Seems it was left behind in #124
true | ||
when nil, "help" | ||
usage | ||
command = args.shift || "help" |
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.
If no command was passed, then default to help
. Keeps the behaviour as before.
lib/packwerk/commands.rb
Outdated
def class_for(command) | ||
class_name = command.sub(" ", "_").underscore.classify + "Command" | ||
if Commands.const_defined?(class_name) | ||
Commands.const_get(class_name) # rubocop:disable Sorbet/ConstantsFromStrings |
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 tried with the registry pattern first, but it required me to require
all the files (with Dir[]
and a glob for example) preemptively.
Using const_get
, we can keep lazy loading.
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 think const_get
is ok, but we don't need this class_for
method, just lean on autoloading to do this work for you. See my other comment. I ran tests and they all pass with the proposed change.
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.
Yeah, so my idea was:
- I wanted to extract the
const_get
+rescue
to its own method to make the thing more readable (I didn't want to pollutePackwerk::Cli#execute_command
with it). It first was a class method onPackwerk::Cli
, and then naturally moved toPackwerk::Commands
. - Once that was done, I noticed I was using exceptions as control flow whereas there is an appropriate method (
#const_defined?
) to check if the constant exists without raisingNameError
, so I used it instead.
I now realized that when I moved the method, I could have omitted the Commands
here:
sig { params(command: String).returns(T.nilable(T.class_of(Commands::BaseCommand))) }
def class_for(command)
class_name = command.sub(" ", "_").underscore.classify + "Command"
- if Commands.const_defined?(class_name)
- Commands.const_get(class_name) # rubocop:disable Sorbet/ConstantsFromStrings
+ if const_defined?(class_name)
+ const_get(class_name) # rubocop:disable Sorbet/ConstantsFromStrings
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 didn't want to pollute
Packwerk::Cli#execute_command
with it
Yeah but now we have a new public method to support, just for a single use case. Feels to me more appropriate just to hide the implementation in the only place that actually uses it (esp since it's just a couple lines).
I noticed I was using exceptions as control flow
Calling an undefined command is an "exception" though, so feels to me that rescuing here is fine (to avoid exposing an exception to the user). I would consider it "control flow" if we were then doing something other work in the rescue
, but we're literally just cleaning up the output.
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.
a new public method to support
That's a very fair point! Maybe I can move the method back to Cli
, make it private, and switch back to rescuing NameError
. I don't have a strong opinion on this bit.
init - set up packwerk | ||
check - run all checks | ||
update-todo - update package_todo.yml files | ||
validate - verify integrity of packwerk and package configuration | ||
version - output packwerk version | ||
help - display help information about packwerk |
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.
Because I kept lazy loading, and commands are not loaded, this help message cannot be generated dynamically.
I'm not super happy about it, does anyone have thoughts?
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.
Maybe we could use the registry pattern just for help messages? So have some registry that commands can call to register their help message, but let the actual command class be autoloaded.
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.
registry pattern
Yup! See my other comment with a link to a specific commit. That's what I used first. But that would mean we need to eager load CLI commands.
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.
Not necessarily. There could be a registry that can be pre-populated for in-gem commands. For commands added externally, the process would be to register your extension (for the help message) and also create an autoloadable constant, which would not be loaded until the command was actually used.
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.
Ahh, so instead of registering the commands from inside the command class, we could do it outside (in a place that we know gets loaded on init), like so?
Commands.register("update-todo", help: "update package_todo.yml files", aliases: ["update"])
For the gem's core commands, these would all be located in the same location, while plugin gems would have to do that somewhere, where they get loaded (eg. their own lib/packwerk/plugin_name.rb
file).
#register
could possibly also offer a way to have command and class name be different, like so:
Commands.register("something", help: "does something", classname: "MyCommandClass")
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.
Yeah right exactly 👍
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 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 autoload tests caught me loading more than I should:
(Didn't know about bin/rake test:loading
.)
So I added one more commit: 721b197 (Move command registration to Cli in order to avoid early loading)
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 module is a temporary to keep the behaviour as it was previously, focusing this PR on a refactor that mostly does not change behaviour.
However, the respective behaviours of check
and update-todo
are expected to diverge:
--offenses-formatter
does not make much sense in the case ofupdate-todo
update-todo
does not accept a list of files (either passed as extra arguments or with--packages
When we work on this separation of behaviours, this module will naturally go away.
lib/packwerk/cli/result.rb
Outdated
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 removed this because it didn't seem to make sense anymore:
- it's never returned by methods that are part of the public API
- it imposes the commands to return their last message wrapped in a
Result
object, instead of outputting it in the relevant IO object that they know about, and to which they keep outputting during their execution
test/unit/packwerk/parse_run_test.rb
Outdated
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 test file, and how I had to move all its tests to CheckCommandTest
and UpdateCommandTest
, represents a +596 −505
diff all by itself, and is what grew this PR to its current +1,063 −777
.
There are a few notable changes in each test though. Here's a single test's diff to illustrate those changes:
--- before.txt 2023-04-20 15:35:47.222809962 +0000
+++ after.txt 2023-04-20 15:36:31.176723735 +0000
@@ -1,4 +1,4 @@
- test "#check does not list stale violations when run on a single file with no violations, even if the containing package has violations" do
+ test "#run does not list stale violations when run on a single file with no violations, even if the containing package has violations" do
use_template(:minimal)
file_to_check = "components/source/some/path.rb"
other_file = "components/source/some/other/path.rb"
@@ -19,27 +19,31 @@
- #{other_file}
YML
+ RunContext.any_instance.stubs(:process_file).returns([])
+ FilesForProcessing.any_instance.stubs(:files).returns(Set.new([file_to_check]))
+
out = StringIO.new
- parse_run = ParseRun.new(
- relative_file_set: Set.new([file_to_check]),
- configuration: Configuration.new({ "parallel" => false }),
- progress_formatter: Formatters::ProgressFormatter.new(out)
+ configuration = Configuration.new({ "parallel" => false })
+ check_command = CheckCommand.new(
+ [],
+ configuration: configuration,
+ out: out,
+ err_out: StringIO.new,
+ progress_formatter: Formatters::ProgressFormatter.new(out),
+ offenses_formatter: configuration.offenses_formatter
)
- RunContext.any_instance.stubs(:process_file).returns([])
- result = parse_run.check
+
+ result = check_command.run
expected_output = <<~EOS
📦 Packwerk is inspecting 1 file
\\.
📦 Finished in \\d+\\.\\d+ seconds
- EOS
- assert_match(/#{expected_output}/, out.string)
- expected_message = <<~EOS
No offenses detected
No stale violations detected
EOS
- assert_equal expected_message, result.message
+ assert_match(/#{expected_output}/, out.string)
- assert result.status
+ assert result
end
lib/packwerk/parse_run.rb
Outdated
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 tempted to rename this file offenses_finder.rb
now. 🤔
That's the only logic that's left in there.
2f07b42
to
c2f99a8
Compare
Specify files or check the include and exclude glob in the config file. | ||
MSG | ||
|
||
true |
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 feels like it should return false
if the above was an error, but this is the existing behaviour.
Also, we pass an err_out
around but it feels like we're not using it as much as we should...
In the spirit of keeping this PR a low impact refactor, I decided not to change those.
|
||
sig { override.returns(T::Boolean) } | ||
def run | ||
@err_out.puts(<<~USAGE) |
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.
Here too, I'm preserving existing behaviour: this help message was output to err_out
before, still is.
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.
Still reading through changes, but made some initial comments. Really great work! ❤️ Way cleaner and more extensible.
autoload_under "commands" do | ||
autoload :OffenseProgressMarker | ||
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.
👍
Seems it was left behind in #124
lib/packwerk/cli.rb
Outdated
when nil, "help" | ||
usage | ||
command = args.shift || "help" | ||
command_class = Commands.class_for(command) |
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.
You can avoid the need for class_for
by just const_get
-ting the command (converted to class name), and rescuing the possible NameError
, like this:
class_name = command.sub(" ", "_").underscore.classify + "Command"
Commands.const_get(class_name).new(
args,
configuration: @configuration,
out: @out,
err_out: @err_out,
progress_formatter: @progress_formatter,
offenses_formatter: @offenses_formatter,
).run
rescue NameError
@err_out.puts("'#{command}' is not a packwerk command. See `packwerk help`.",)
false
end
lib/packwerk/commands.rb
Outdated
def class_for(command) | ||
class_name = command.sub(" ", "_").underscore.classify + "Command" | ||
if Commands.const_defined?(class_name) | ||
Commands.const_get(class_name) # rubocop:disable Sorbet/ConstantsFromStrings |
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 think const_get
is ok, but we don't need this class_for
method, just lean on autoloading to do this work for you. See my other comment. I ran tests and they all pass with the proposed change.
init - set up packwerk | ||
check - run all checks | ||
update-todo - update package_todo.yml files | ||
validate - verify integrity of packwerk and package configuration | ||
version - output packwerk version | ||
help - display help information about packwerk |
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.
Maybe we could use the registry pattern just for help messages? So have some registry that commands can call to register their help message, but let the actual command class be autoloaded.
lib/packwerk/commands.rb
Outdated
autoload :CheckCommand | ||
autoload :HelpCommand | ||
autoload :InitCommand | ||
autoload :UpdateCommand, "packwerk/commands/update_todo_command" |
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.
Maybe we should add some aliasing functionality to make this more natural and explicit? i.e. here we're aliasing update
to update_todo
, so rather than adding an autoload we could have execute_command
look at a list of aliases and "redirect" the command accordingly if an alias exists.
Or do we also need to support the existence of the UpdateComand
constant as well? 🤔
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.
Or do we also need to support the existence of the
UpdateCommand
constant as well? 🤔
We don't. It's a class introduced in this PR, but doesn't have to.
My problem is as follows:
- we use autoloading, so the class is not loaded until we have defined that it's necessary.
- if we implement a system to declare aliases, then we need it to be run during initialization
- I would like to avoid having to declare aliases outside of the command class itself (open-closed principle)
I guess the fact that we need these autoload
is already an infringement to the open-closed principle though... 🤔
Check this commit to see an early idea I had, where each command was able to declare itself, and its aliases in the process. The caveat was that command classes needed to be eager loaded.
24d4033#diff-c990d324e3c6c514f831ceaa8414cb9450101d4f76e2ac32e80c812f2f455125R10
Note: this changes `packwerk help`'s output from stderr to stdout
Wondering if I should also deprecate #mark_as_inspected and
Note that I also moved and adjusted the corresponding tests to `CheckCommandTest`
… decide how/when/where to output messages
This time, registration happens from outside the command class, and using a classname string, so that we can keep the lazy loading behaviour.
c2f99a8
to
d5c7dbe
Compare
lib/packwerk/cli.rb
Outdated
extend T::Sig | ||
sig { params(name: String, help: String, aliases: T::Array[String]).void } | ||
def register_command(name, help:, aliases: []) | ||
CommandRegistry.register(name, help: help, aliases: aliases) |
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 just delegates to CommandRegistry
. It allows me to keep that class private, while it exposes public methods to be used within Packwerk.
6e1f7ae
to
721b197
Compare
lib/packwerk/cli.rb
Outdated
end | ||
|
||
Cli.register_command("init", help: "set up packwerk") |
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 help text should be declared on the command class. It makes it easier to change and expand on.
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.
See previous conversation with @shioyama : if I do so (which I'm not against), I'll need to either eager load command classes or at least lazy load them all when the user called for help.
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.
In the case of the help command, I think it is fine to load all commands. Since this is done lazily I don't see a problem.
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.
lib/packwerk/cli/command_registry.rb
Outdated
sig { returns(T.class_of(Cli::BaseCommand)) } | ||
def command_class | ||
classname = @name.sub(" ", "_").underscore.classify + "Command" | ||
Cli.const_get(classname) # rubocop:disable Sorbet/ConstantsFromStrings |
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.
Can you explain your motivation to not use a Packwerk::Cli::Commands
module to const_get from? We could fit all command related code in one place (eg. Commands::Registry
, Commands::Base
, Commands::HelpCommand
, etc. It seems safer and more organized.
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.
Without the need to auto match a command name with a class name, and with instead an explicit registration of each command, I felt Commands
had lost its main purpose, that triggered its creation: hosting all commands and nothing else so that packwerk not-a-command-but-const-with-matching-name-exists
would fail beautifully and not weirdly.
With its main purpose lost, I went for the "less code is better" approach.
To me, commands are part of the CLI (command line interface 😬), so using Cli
as namespace felt adequate.
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.
Personally, I think the organization of the commands/
namespace makes command specific code easier to reason about, but I don't feel strongly about it. I'm slightly worried about this from a public API perspective.
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.
Cli
is public, sure, but if I make my constants nested under it private, it shouldn't make a difference?
I'm not antagonizing, just trying to understand where the problem would be exactly.
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.
lib/packwerk/cli/check_command.rb
Outdated
end | ||
|
||
run_context = RunContext.from_configuration(@configuration) | ||
offense_collection = OffenseCollection.new(@configuration.root_path) |
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.
These could be private methods.
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.
Quite verbose, but possible: c8b3864
lib/packwerk/cli/uses_parse_run.rb
Outdated
end | ||
|
||
sig { returns(T::Hash[Symbol, T.untyped]) } | ||
def parse_options |
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.
Thinking ahead slightly, it might make sense for the base class to implement def options; raise NotImplementedError; end
(or Sorbet equivalent abstract class syntax) and have this be a memoized public method for commands to implement.
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.
That'll come in a next PR where I split the options used by check
and update-todo
into separate option parsers. This PR felt already big enough.
I did not split in this PR too because splitting will also induce behavior change (I don't want update-todo you use --packages
and --offenses-formatter
anymore), and I'd like to make sure I get proper review on these bits.
lib/packwerk/cli/command_registry.rb
Outdated
|
||
module Packwerk | ||
class Cli | ||
class CommandRegistry |
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 instance of this class is an entry, not a registry, so it doesn't really make sense to me. I would say that a better api would be Commands.all
/ Commands.register
, and then name this class Commands::Info
, Commands::Lookup
, or Commands::Entry
(etc.) so the registry can live on Commands
.
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.
Yeah, the naming didn't please me. I'll take another look.
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 renamed CommandRegistry
to LazyLoadedCommand
(to make the distinction with BaseCommand
). d49a7ea
I prefer typing the registry as a T::Array[LazyLoadedCommand]
rather than a T::Array[T::Hash[Symbol, T.untyped]]
(where each value in the hash would look like { name: "foo", aliases: ["bar"] }
).
I guess I could restore the Commands
module/namespace anyway and have the registry live there... (But I'd rather each entry be a LazyLoadedCommand
than a primitive.)
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.
and then name this class
Commands::Info
,Commands::Lookup
Just understood what you meant here. Could work too. 🤔 Will try.
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.
end | ||
|
||
all_offenses = T.let([], T::Array[Offense]) | ||
on_interrupt = T.let(-> { @progress_formatter.interrupted }, T.proc.void) |
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.
on_interrupt = T.let(-> { @progress_formatter.interrupted }, T.proc.void) | |
on_interrupt = T.let(proc { @progress_formatter.interrupted }, T.proc.void) |
These could also be memoized private methods
|
||
module Packwerk | ||
module Commands | ||
class LazyLoadedEntry |
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.
class LazyLoadedEntry | |
class Entry |
Nit: I think LazyLoadedEntry
is a little too verbose, but feel free to ignore this. I like the implementation!
end | ||
def initialize(args, configuration:, out:, err_out:, progress_formatter:, offenses_formatter:) | ||
@args = args | ||
@configuration = configuration |
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.
These should be assigned by super, right?
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.
Hmm, yeah, I think so, but there was a reason why I did that. Will get back to you when I remember. 😅
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.
Ahh, yes, it's a Sorbet problem. If I don't make these assignments in this initialize
method (ie if for example I comment these two lines), Sorbet will error with the following:
bin/srb tc 130 ↵
lib/packwerk/commands/uses_parse_run.rb:29: Use of undeclared variable @configuration https://srb.help/6002
29 | @configuration.parallel = @parsed_options[:parallel]
^^^^^^^^^^^^^^
Note:
Use T.let to declare this variable.
For more information, see https://sorbet.org/docs/type-annotations
lib/packwerk/commands/uses_parse_run.rb:41: Use of undeclared variable @configuration https://srb.help/6002
41 | configuration: @configuration
^^^^^^^^^^^^^^
Note:
Use T.let to declare this variable.
For more information, see https://sorbet.org/docs/type-annotations
lib/packwerk/commands/uses_parse_run.rb:56: Use of undeclared variable @configuration https://srb.help/6002
56 | parallel: @configuration.parallel?,
^^^^^^^^^^^^^^
Note:
Use T.let to declare this variable.
For more information, see https://sorbet.org/docs/type-annotations
lib/packwerk/commands/uses_parse_run.rb:66: Use of undeclared variable @configuration https://srb.help/6002
66 | options[:parallel] = T.let(@configuration.parallel?, T::Boolean)
^^^^^^^^^^^^^^
Note:
Use T.let to declare this variable.
For more information, see https://sorbet.org/docs/type-annotations
lib/packwerk/commands/uses_parse_run.rb:82: Use of undeclared variable @args https://srb.help/6002
82 | end.parse!(@args)
^^^^^
Note:
Use T.let to declare this variable.
For more information, see https://sorbet.org/docs/type-annotations
Errors: 5
Here's a minimal reproduction on Sorbet.run:
# typed: strict
class Foo
extend T::Sig
sig { params(param: String).void }
def initialize(param)
@param = param
end
end
module Bar
extend T::Sig
extend T::Helpers
requires_ancestor { Foo }
sig { params(param: String).void }
def initialize(param)
# @param = param # This line fixes the Sorbet error on line 22
super
@param << " concatenated" # Error "Use of undeclared variable @param"
end
end
I think using attribute readers as you suggested in another comment could allow us to avoid this repetition. 🤔
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 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.
Hm, I wonder if this is a bug in Sorbet. If we require the class as an ancestor, it should always expect the initializer to run.
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.
True, I guess it can't know for sure. The workaround is good 👍
@out = out | ||
@err_out = err_out | ||
@progress_formatter = progress_formatter | ||
@offenses_formatter = offenses_formatter |
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 interested in how you feel about using ivars for stdout/stderr/args/formatters? Should we create private/protected/public readers for these ivars? It seems like it would be a nicer public API to me.
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.
Yeah, I like the idea. I tend to define accessors for everything usually. I did not do it here because Sorbet requires a level of verbosity I was not super comfortable with. I'll give it a try.
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.
end | ||
def initialize(args, configuration:, out:, err_out:, progress_formatter:, offenses_formatter:) | ||
super | ||
@_parsed_options = T.let(nil, T.nilable(T::Hash[Symbol, T.untyped])) |
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.
@_parsed_options = T.let(nil, T.nilable(T::Hash[Symbol, T.untyped])) | |
@parsed_options = T.let(nil, T.nilable(T::Hash[Symbol, T.untyped])) |
I don't think this needs to be underscored. No other ivar is.
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.
Ah, I used an underscore because it's a memoization ivar for the parsed_options
method, and Sorbet seemed to require this T.let
in the initializer. This might not be true though, I'll check.
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.
@@ -44,6 +53,7 @@ def mark_as_failed | |||
def interrupted | |||
@out.puts | |||
@out.puts("Manually interrupted. Violations caught so far are listed below:") | |||
@out.puts |
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.
Are these extra lines necessary, or just for readability?
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.
What are you trying to accomplish?
Packwerk's code around the CLI is currently hard to understand, mixes concepts together, and is overall very difficult to change. (Changing a seemingly simple thing in the CLI would sometimes requires editing many files).
This PR attempts a refactor of the code CLI in order to make it
What approach did you choose and why?
The first approach that we tried with @gmcgibbon involved using Thor, but we realized that using Thor while not breaking public APIs would be overly complicated, and end up trading one type of complexity for another.
In this PR, my approach was mainly to:
What should reviewers focus on?
I know this looks rather big, but to this PR's defense, about 500 lines additions/deletions are test code lines that moved to a different file.
I left a few comments which you can check below about the things I'm unsure and would like a second opinion about.
If the approach sounds useful to you, you can also check the changes commit by commit, as I have made each commit a logical and justified change, ensuring that each commit also keeps the test suite green.
Type of Change
Checklist