Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', head]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- uses: ruby/setup-ruby@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/spec/reports/
/tmp/
*.gem
.DS_Store
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ gem install ruboclean
## Command synopsis

```shell
ruboclean [path] \
ruboclean [input] \
[--stdin] \
[--output=/path/to/file.yml] \
[--silent] \
[--preserve-comments] \
Expand All @@ -94,7 +95,7 @@ ruboclean [path] \

### Parameters

#### `path`
#### `input`

Can be a directory that contains a `.rubocop.yml`, or a path to a `.rubocop.yml` directly.
Defaults to the current working directory.
Expand All @@ -107,11 +108,29 @@ ruboclean /path/to/dir # uses `.rubocop.yml` of /path/to/dir
ruboclean /path/to/dir/.rubocop.yml
```

#### `--stdin`

It's possible to read from `STDIN`, for example:

```shell
echo "SomeConfig: True" | ruboclean --stdin
```

Using `STDIN` will automatically set the output to `STDOUT`. You can use the `--output` flag to override this.

Also, if you use `STDIN`, your current working directory should be the root directory of your project, so that the
cleanup of unused paths/references (see `--preserve-paths`) works properly. If your current working directory
is something else, you have to explicitly provide the project's root directory using the `input` argument.

```shell
echo "SomeConfig: True" | ruboclean /path/to/the/project/directory --stdin
```

#### `--output=/path/to/file.yml`

Output path where the result is written to.
Can be absolute or relative to the current working directory.
`--output=STDOUT` prints it to STDOUT and not to a file.
`--output=STDOUT` prints it to `STDOUT` and not to a file, and silences all logging (see `--silent`).

##### Examples

Expand All @@ -124,7 +143,7 @@ ruboclean --output=STDOUT # does not write anything to a file
#### `--silent`

Suppress any log output displayed on the screen when executing the command.
It still prints to STDOUT if used in combination with `--output=STDOUT`.
Using `--output=STDOUT` also forces this.

#### `--preserve-comments`

Expand Down
5 changes: 4 additions & 1 deletion lib/ruboclean.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
require "ruboclean/grouper"
require "ruboclean/path_cleanup"
require "ruboclean/runner"
require "ruboclean/runner/options"
require "ruboclean/runner/options/input"
require "ruboclean/runner/options/output"
require "ruboclean/stream_writer"
require "ruboclean/to_yaml_converter"
require "ruboclean/version"
Expand All @@ -16,7 +19,7 @@ def self.run_from_cli!(args)
runner = Runner.new(args)
logger = Ruboclean::Logger.new(runner.verbose? ? :verbose : :none)

logger.verbose "Using path '#{runner.path}' ... "
logger.verbose "Using input path '#{runner.input_path}' ... "
changed = runner.run!
logger.verbose post_execution_message(changed, runner.verify?)

Expand Down
23 changes: 12 additions & 11 deletions lib/ruboclean/cli_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Ruboclean
# Reads command line arguments and exposes corresponding reader methods
class CliArguments
FLAG_DEFAULTS = {
"--stdin" => false,
"--output" => nil,
"--silent" => false,
"--preserve-comments" => false,
Expand All @@ -15,16 +16,16 @@ def initialize(command_line_arguments = [])
@command_line_arguments = Array(command_line_arguments)
end

def path
@path ||= find_path
def input
@input ||= find_input
end

def output_path
flag_arguments.fetch("--output")
def stdin?
flag_arguments.fetch("--stdin")
end

def verbose?
!silent?
def output
flag_arguments.fetch("--output")
end

def silent?
Expand All @@ -47,12 +48,12 @@ def verify?

attr_reader :command_line_arguments

def find_path
command_line_arguments.first.then do |argument|
return Dir.pwd if argument.nil? || argument.start_with?("--")
def find_input
first_argument = command_line_arguments.first

argument
end
return nil if first_argument.nil? || first_argument.start_with?("--")

first_argument
end

def flag_arguments
Expand Down
12 changes: 9 additions & 3 deletions lib/ruboclean/path_cleanup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ module Ruboclean
# If all entries in `Include` or `Exclude` are removed, the entire property is removed.
# If a Cop gets entirely truncated due to removing all `Includes` and/or `Exclude`, the Cop itself will be removed.
class PathCleanup
def initialize(configuration_hash, root_directory)
def initialize(configuration_hash, options:)
@configuration_hash = configuration_hash
@root_directory = root_directory
@options = options
end

def cleanup
return configuration_hash if options.preserve_paths?

configuration_hash.each_with_object({}) do |(top_level_key, top_level_value), hash|
result = process_top_level_value(top_level_value)

Expand All @@ -25,7 +27,11 @@ def cleanup

private

attr_reader :configuration_hash, :root_directory
attr_reader :configuration_hash, :options

def root_directory
options.input_path.dirname
end

# top_level_value could be something like this:
#
Expand Down
72 changes: 19 additions & 53 deletions lib/ruboclean/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,46 @@
module Ruboclean
# Entry point for processing
class Runner
def initialize(args = [])
@cli_arguments = CliArguments.new(args)
def initialize(command_line_arguments = [])
cli_arguments = CliArguments.new(command_line_arguments)
@options = Runner::Options.new(cli_arguments: cli_arguments)
end

def run!
return if source_file_pathname.empty?
return if source_yaml.empty?

load_file.then(&method(:order))
.then(&method(:cleanup_paths))
.then(&method(:convert_to_yaml))
.then(&method(:write_stream!))
.then(&method(:changed?))
parse_yaml.then(&method(:order))
.then(&method(:cleanup_paths))
.then(&method(:convert_to_yaml))
.then(&method(:write_stream!))
.then(&method(:changed?))
end

def changed?(target_yaml)
target_yaml != source_yaml
end

def verbose?
cli_arguments.verbose?
options.verbose?
end

def verify?
cli_arguments.verify?
options.verify?
end

def path
cli_arguments.path
def input_path
options.input_path
end

private

attr_reader :cli_arguments
attr_reader :options

def source_yaml
@source_yaml ||= source_file_pathname.read
@source_yaml ||= options.input_stream.read
end

def load_file
def parse_yaml
YAML.safe_load(source_yaml, permitted_classes: [Regexp])
end

Expand All @@ -53,50 +54,15 @@ def order(configuration_hash)
end

def cleanup_paths(configuration_hash)
return configuration_hash if cli_arguments.preserve_paths?

PathCleanup.new(configuration_hash, source_file_pathname.dirname).cleanup
PathCleanup.new(configuration_hash, options: options).cleanup
end

def convert_to_yaml(configuration_hash)
ToYamlConverter.new(configuration_hash, cli_arguments.preserve_comments?, source_yaml).to_yaml
ToYamlConverter.new(configuration_hash, source_yaml, options: options).to_yaml
end

def write_stream!(target_yaml)
target_yaml.tap do |content|
StreamWriter.new(target_file_pathname, content).write! unless verify?
end
end

# TODO: Find a better place to compute source_file_pathname and target_file_pathname
# Preferrably it should happen early in the lifecycle, as it includes (and raises) argument validations
def source_file_pathname
@source_file_pathname ||= find_source_file_pathname
end

def target_file_pathname
@target_file_pathname ||= find_target_file_pathname
end

def find_source_file_pathname
source_path = Pathname.new(cli_arguments.path)

source_path = source_path.join(".rubocop.yml") if source_path.directory?

return source_path if source_path.exist?

raise ArgumentError, "path does not exist: '#{source_path}'"
end

def find_target_file_pathname
return source_file_pathname if cli_arguments.output_path.nil?

target_path = Pathname.new(cli_arguments.output_path)
target_path = Pathname.new(Dir.pwd).join(target_path) if target_path.relative?

return target_path unless target_path.directory?

raise ArgumentError, "output path (--output=#{target_path}) cannot be a directory"
StreamWriter.new(target_yaml, options: options).write!
end
end
end
56 changes: 56 additions & 0 deletions lib/ruboclean/runner/options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Ruboclean
class Runner
# Consolidates cli_arguments with "smart" defaults
class Options
def initialize(cli_arguments:)
@cli_arguments = cli_arguments
end

def input_stream
input.stream
end

def input_path
input.input_path
end

def output_stream
output.stream
end

def verbose?
!silent?
end

def silent?
cli_arguments.silent? || output.stdout?
end

def preserve_comments?
cli_arguments.preserve_comments?
end

def preserve_paths?
cli_arguments.preserve_paths?
end

def verify?
cli_arguments.verify?
end

private

attr_reader :cli_arguments

def input
@input ||= Ruboclean::Runner::Options::Input.new(cli_arguments: cli_arguments)
end

def output
@output ||= Ruboclean::Runner::Options::Output.new(cli_arguments: cli_arguments, input: input)
end
end
end
end
44 changes: 44 additions & 0 deletions lib/ruboclean/runner/options/input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Ruboclean
class Runner
class Options
# Determines the input stream
class Input
def initialize(cli_arguments:)
@cli_arguments = cli_arguments
end

def stream
@stream ||= determine_input_stream
end

def input_path
@input_path ||= find_input_path
end

def stdin?
cli_arguments.stdin?
end

private

attr_reader :cli_arguments

def determine_input_stream
return $stdin if stdin?

return input_path if input_path.exist?

raise ArgumentError, "input path does not exist: '#{input_path}'"
end

def find_input_path
pathname = cli_arguments.input.nil? ? Pathname.new(Dir.pwd) : Pathname.new(cli_arguments.input)
pathname = pathname.join(".rubocop.yml") if pathname.directory?
pathname
end
end
end
end
end
Loading