rubocounsel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/CLAUDE.md +11 -0
- data/.claude/settings.json +22 -0
- data/.rubocounsel.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/FEEDBACK.md +67 -0
- data/LICENSE.txt +21 -0
- data/PLAN.md +346 -0
- data/README.md +78 -0
- data/Rakefile +12 -0
- data/example.png +0 -0
- data/exe/rubocounsel +6 -0
- data/lib/rubocounsel/cli.rb +94 -0
- data/lib/rubocounsel/config_writer.rb +69 -0
- data/lib/rubocounsel/configurable_attribute.rb +44 -0
- data/lib/rubocounsel/cop_catalog.rb +73 -0
- data/lib/rubocounsel/cop_entry.rb +80 -0
- data/lib/rubocounsel/counselor.rb +214 -0
- data/lib/rubocounsel/example.rb +63 -0
- data/lib/rubocounsel/example_loader.rb +109 -0
- data/lib/rubocounsel/runner.rb +34 -0
- data/lib/rubocounsel/version.rb +5 -0
- data/lib/rubocounsel.rb +16 -0
- data/rubocounsel.png +0 -0
- data/sig/rubocounsel.rbs +4 -0
- metadata +126 -0
data/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# RuboCounsel
|
|
2
|
+
|
|
3
|
+
<img src="rubocounsel.png" alt="RuboCounsel" width="400">
|
|
4
|
+
|
|
5
|
+
An interactive CLI tool that transforms the RuboCop experience from "fix these errors" to "let's decide on your team's style together." Instead of dumping a wall of offenses, RuboCounsel walks you through each failing cop, explains what it does, shows real code examples, and helps you make informed decisions that get written to `.rubocop.yml`.
|
|
6
|
+
|
|
7
|
+
The result: a RuboCop configuration that reflects conscious choices, not cargo-culted defaults.
|
|
8
|
+
|
|
9
|
+
<img src="example.png" alt="RuboCounsel example" width="600">
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add the gem to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle add rubocounsel
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install rubocounsel
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
Run RuboCounsel against your project:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
rubocounsel
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or specify paths:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
rubocounsel app/ lib/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Any RuboCop flags are passed through, so you can filter to specific cops or use a custom config:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
rubocounsel --only Style/StringLiterals
|
|
43
|
+
rubocounsel --config .rubocop_strict.yml app/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
RuboCounsel will:
|
|
47
|
+
|
|
48
|
+
1. Run RuboCop and identify which cops have offenses
|
|
49
|
+
2. Skip any cops you've already configured in `.rubocop.yml`
|
|
50
|
+
3. Walk you through each remaining cop interactively
|
|
51
|
+
|
|
52
|
+
For each cop, you'll see its documentation and a link to the full docs, then choose to:
|
|
53
|
+
|
|
54
|
+
- **Configure** — set the cop's attributes (style, max length, etc.) via guided prompts
|
|
55
|
+
- **Disable** — add `Enabled: false` to your config
|
|
56
|
+
- **Skip** — leave it unconfigured for now
|
|
57
|
+
|
|
58
|
+
Configuration choices are saved to `.rubocop.yml` after each cop, so you won't lose progress if you quit early. Re-running RuboCounsel will pick up where you left off, skipping cops that are already configured.
|
|
59
|
+
|
|
60
|
+
RuboCounsel supports extension gems like `rubocop-rails`, `rubocop-rspec`, and `rubocop-performance` — any cops registered in your bundle will be included automatically.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
65
|
+
|
|
66
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
67
|
+
|
|
68
|
+
## Contributing
|
|
69
|
+
|
|
70
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dewyze/rubocounsel. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/dewyze/rubocounsel/blob/main/CODE_OF_CONDUCT.md).
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
75
|
+
|
|
76
|
+
## Code of Conduct
|
|
77
|
+
|
|
78
|
+
Everyone interacting in the RuboCounsel project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dewyze/rubocounsel/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/example.png
ADDED
|
Binary file
|
data/exe/rubocounsel
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cli/ui"
|
|
4
|
+
|
|
5
|
+
module RuboCounsel
|
|
6
|
+
# Command-line interface that ties all components together.
|
|
7
|
+
#
|
|
8
|
+
# Responsibilities:
|
|
9
|
+
# - Run the Runner to collect offending cops
|
|
10
|
+
# - Pass offending cop names to the Counselor
|
|
11
|
+
# - Pass Counselor's choices to ConfigWriter
|
|
12
|
+
# - Output the generated config
|
|
13
|
+
class CLI
|
|
14
|
+
HELP = <<~TEXT
|
|
15
|
+
Usage: rubocounsel [options] [paths]
|
|
16
|
+
|
|
17
|
+
RuboCounsel walks you through each offending RuboCop cop interactively,
|
|
18
|
+
helping you configure, disable, or skip each one.
|
|
19
|
+
|
|
20
|
+
All RuboCop flags are passed through (e.g. --only, --except, --config).
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-h, --help Show this help message
|
|
24
|
+
-v, --version Show version
|
|
25
|
+
TEXT
|
|
26
|
+
|
|
27
|
+
def self.run(args, output: $stdout)
|
|
28
|
+
if args.include?("--help") || args.include?("-h")
|
|
29
|
+
output.puts HELP
|
|
30
|
+
return 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if args.include?("--version") || args.include?("-v")
|
|
34
|
+
output.puts "rubocounsel #{RuboCounsel::VERSION}"
|
|
35
|
+
return 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
options, paths = RuboCop::Options.new.parse(args)
|
|
39
|
+
paths = ["."] if paths.empty?
|
|
40
|
+
|
|
41
|
+
new(paths: paths, rubocop_options: options, output: output).run
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(paths: ["."], rubocop_options: {}, counselor_factory: nil, output: $stdout)
|
|
45
|
+
@paths = paths
|
|
46
|
+
@rubocop_options = rubocop_options
|
|
47
|
+
@counselor_factory = counselor_factory || method(:default_counselor)
|
|
48
|
+
@output = output
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run
|
|
52
|
+
::CLI::UI::StdoutRouter.enable
|
|
53
|
+
|
|
54
|
+
cop_names = collect_offending_cops
|
|
55
|
+
return 0 if cop_names.empty?
|
|
56
|
+
|
|
57
|
+
all_choices = {}
|
|
58
|
+
counsel_cops(cop_names) do |cop_name, choice|
|
|
59
|
+
all_choices[cop_name] = choice
|
|
60
|
+
output_config(all_choices)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if all_choices.empty?
|
|
64
|
+
@output.puts "All offending cops are already configured."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
0
|
|
68
|
+
rescue Interrupt
|
|
69
|
+
@output.puts "\nExiting..."
|
|
70
|
+
0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def collect_offending_cops
|
|
76
|
+
runner = Runner.new(rubocop_options: @rubocop_options)
|
|
77
|
+
runner.run(@paths)
|
|
78
|
+
runner.offending_cops.to_a
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def counsel_cops(cop_names, &block)
|
|
82
|
+
counselor = @counselor_factory.call(cop_names)
|
|
83
|
+
counselor.counsel(&block)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def default_counselor(cop_names)
|
|
87
|
+
Counselor.new(cop_names)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def output_config(choices)
|
|
91
|
+
ConfigWriter.new(choices).merge_and_write(".rubocop.yml")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "active_support/core_ext/hash/deep_merge"
|
|
6
|
+
|
|
7
|
+
module RuboCounsel
|
|
8
|
+
# Generates .rubocop.yml content from user choices.
|
|
9
|
+
#
|
|
10
|
+
# Converts a hash of cop choices to YAML format and optionally writes to file.
|
|
11
|
+
# The choices hash maps cop names to their configuration settings:
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "Style/StringLiterals" => { "EnforcedStyle" => "double_quotes" },
|
|
15
|
+
# "Metrics/MethodLength" => { "Max" => 15 }
|
|
16
|
+
# }
|
|
17
|
+
class ConfigWriter
|
|
18
|
+
PRIORITY_KEYS = ["inherit_from", "inherit_mode", "require", "AllCops"].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :choices
|
|
21
|
+
|
|
22
|
+
def initialize(choices)
|
|
23
|
+
@choices = choices
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_yaml
|
|
27
|
+
return "" if choices.empty?
|
|
28
|
+
|
|
29
|
+
add_blank_lines(sort_config(choices).to_yaml)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def write(path)
|
|
33
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
34
|
+
File.write(path, to_yaml)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Merges choices into an existing config file and writes it back.
|
|
38
|
+
def merge_and_write(path)
|
|
39
|
+
existing = File.exist?(path) ? YAML.load_file(path) || {} : {}
|
|
40
|
+
merged = sort_config(existing.deep_merge(choices))
|
|
41
|
+
File.write(path, add_blank_lines(merged.to_yaml))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def sort_config(config)
|
|
47
|
+
priority = {}
|
|
48
|
+
cops = {}
|
|
49
|
+
|
|
50
|
+
config.each do |key, value|
|
|
51
|
+
if PRIORITY_KEYS.include?(key)
|
|
52
|
+
priority[key] = value
|
|
53
|
+
else
|
|
54
|
+
cops[key] = value
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
ordered = PRIORITY_KEYS.each_with_object({}) do |key, hash|
|
|
59
|
+
hash[key] = priority[key] if priority.key?(key)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
ordered.merge(cops.sort.to_h)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add_blank_lines(yaml)
|
|
66
|
+
yaml.gsub(/^([A-Za-z])/, "\n\\1").lstrip
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCounsel
|
|
4
|
+
# Represents one configurable aspect of a cop (e.g., EnforcedStyle, Max).
|
|
5
|
+
#
|
|
6
|
+
# Determines the attribute type based on its value:
|
|
7
|
+
# - :choice - has a list of valid options (EnforcedStyle with SupportedStyles)
|
|
8
|
+
# - :boolean - true/false value
|
|
9
|
+
# - :integer - numeric value
|
|
10
|
+
# - :array - list of values (AllowedMethods, etc.)
|
|
11
|
+
# - :string - any other string value
|
|
12
|
+
class ConfigurableAttribute
|
|
13
|
+
attr_reader :name, :default_value, :options
|
|
14
|
+
|
|
15
|
+
def initialize(name, default_value, options: nil)
|
|
16
|
+
@name = name
|
|
17
|
+
@default_value = default_value
|
|
18
|
+
@options = options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def type
|
|
22
|
+
return :choice if @options
|
|
23
|
+
|
|
24
|
+
case @default_value
|
|
25
|
+
when true, false
|
|
26
|
+
:boolean
|
|
27
|
+
when Integer
|
|
28
|
+
:integer
|
|
29
|
+
when Array
|
|
30
|
+
:array
|
|
31
|
+
else
|
|
32
|
+
:string
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def choice?
|
|
37
|
+
type == :choice
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def boolean?
|
|
41
|
+
type == :boolean
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/inclusion"
|
|
4
|
+
require "rubocop"
|
|
5
|
+
|
|
6
|
+
module RuboCounsel
|
|
7
|
+
# Central repository of cop information.
|
|
8
|
+
# Uses RuboCop's Registry for cop discovery and ExampleLoader for documentation.
|
|
9
|
+
#
|
|
10
|
+
# The Registry is RuboCop's authoritative cop list - stable and complete.
|
|
11
|
+
# Examples come from YARD parsing (separate concern, can fail gracefully).
|
|
12
|
+
class CopCatalog
|
|
13
|
+
# InternalAffairs cops are for RuboCop development, not end users.
|
|
14
|
+
EXCLUDED_COP_DEPARTMENTS = ["InternalAffairs"].freeze
|
|
15
|
+
|
|
16
|
+
def initialize(example_loader: ExampleLoader.instance)
|
|
17
|
+
@example_loader = example_loader
|
|
18
|
+
@cops = nil
|
|
19
|
+
@examples_by_class_name = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns a Hash where keys are cop names (e.g., "Style/StringLiterals")
|
|
23
|
+
# and values are cop classes.
|
|
24
|
+
def cops
|
|
25
|
+
@cops ||= load_cops
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the cop class for the given name, or nil if not found.
|
|
29
|
+
def lookup(cop_name)
|
|
30
|
+
cops[cop_name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns an Array of Example objects for the given cop name.
|
|
34
|
+
# Returns empty array if no examples found.
|
|
35
|
+
def examples_for(cop_name)
|
|
36
|
+
cop_class = lookup(cop_name)
|
|
37
|
+
return [] unless cop_class
|
|
38
|
+
|
|
39
|
+
examples_by_class_name[cop_class.to_s] || []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the full YARD docstring for the given cop name.
|
|
43
|
+
# Returns nil if no docstring found.
|
|
44
|
+
def docstring_for(cop_name)
|
|
45
|
+
cop_class = lookup(cop_name)
|
|
46
|
+
return nil unless cop_class
|
|
47
|
+
|
|
48
|
+
docstrings_by_class_name[cop_class.to_s]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def load_cops
|
|
54
|
+
result = {}
|
|
55
|
+
|
|
56
|
+
RuboCop::Cop::Registry.global.each do |cop_class|
|
|
57
|
+
next if cop_class.department.to_s.in?(EXCLUDED_COP_DEPARTMENTS)
|
|
58
|
+
|
|
59
|
+
result[cop_class.cop_name] = cop_class
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def examples_by_class_name
|
|
66
|
+
@examples_by_class_name ||= @example_loader.examples
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def docstrings_by_class_name
|
|
70
|
+
@docstrings_by_class_name ||= @example_loader.docstrings
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubocop"
|
|
4
|
+
|
|
5
|
+
module RuboCounsel
|
|
6
|
+
# Represents a single cop with its configuration and examples.
|
|
7
|
+
#
|
|
8
|
+
# This is an entry in the CopCatalog - a data object that combines:
|
|
9
|
+
# - The cop class (from Registry)
|
|
10
|
+
# - Its configuration (from RuboCop's default config)
|
|
11
|
+
# - Its examples (from YARD parsing)
|
|
12
|
+
# - Computed configurable attributes
|
|
13
|
+
class CopEntry
|
|
14
|
+
# Keys in cop configuration that are metadata, not user-configurable options.
|
|
15
|
+
METADATA_KEYS = [
|
|
16
|
+
"Description",
|
|
17
|
+
"StyleGuide",
|
|
18
|
+
"Enabled",
|
|
19
|
+
"VersionAdded",
|
|
20
|
+
"VersionChanged",
|
|
21
|
+
"VersionRemoved",
|
|
22
|
+
"Reference",
|
|
23
|
+
"Safe",
|
|
24
|
+
"SafeAutoCorrect"
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :cop_class, :examples
|
|
28
|
+
|
|
29
|
+
def initialize(cop_class, examples: [])
|
|
30
|
+
@cop_class = cop_class
|
|
31
|
+
@examples = examples
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cop_name
|
|
35
|
+
@cop_class.cop_name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def configuration
|
|
39
|
+
@configuration ||= RuboCop::ConfigLoader.default_configuration[cop_name].to_h
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def configurable_attributes
|
|
43
|
+
@configurable_attributes ||= build_configurable_attributes
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_configurable_attributes
|
|
49
|
+
attributes = []
|
|
50
|
+
|
|
51
|
+
configuration.each do |key, value|
|
|
52
|
+
next if metadata_key?(key)
|
|
53
|
+
next if supported_key?(key)
|
|
54
|
+
|
|
55
|
+
options = find_options_for(key)
|
|
56
|
+
attributes << ConfigurableAttribute.new(key, value, options: options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attributes
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def metadata_key?(key)
|
|
63
|
+
METADATA_KEYS.include?(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def supported_key?(key)
|
|
67
|
+
key.start_with?("Supported")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Finds the corresponding SupportedXxx options for an EnforcedXxx key.
|
|
71
|
+
# E.g., EnforcedStyle -> SupportedStyles
|
|
72
|
+
def find_options_for(key)
|
|
73
|
+
return nil unless key.start_with?("Enforced")
|
|
74
|
+
|
|
75
|
+
# EnforcedStyle -> SupportedStyles, EnforcedHashRocketStyle -> SupportedHashRocketStyles
|
|
76
|
+
supported_key = key.sub("Enforced", "Supported") + "s"
|
|
77
|
+
configuration[supported_key]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cli/ui"
|
|
4
|
+
require "rubocop"
|
|
5
|
+
|
|
6
|
+
module RuboCounsel
|
|
7
|
+
# The interactive CLI experience for walking through offending cops.
|
|
8
|
+
#
|
|
9
|
+
# Responsibilities:
|
|
10
|
+
# - Walk through each offending cop
|
|
11
|
+
# - Display cop information and examples
|
|
12
|
+
# - Collect user choices via cli-ui prompts
|
|
13
|
+
# - Return choices hash for ConfigWriter
|
|
14
|
+
#
|
|
15
|
+
# Cops that are already configured in .rubocop.yml are skipped by default.
|
|
16
|
+
class Counselor
|
|
17
|
+
ACTIONS = ["Configure", "Disable", "Skip"].freeze
|
|
18
|
+
FILE_TARGETING_ATTRIBUTES = ["Include", "Exclude"].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(cop_names, catalog: CopCatalog.new, user_config: nil, output: $stdout)
|
|
21
|
+
@cop_names = cop_names
|
|
22
|
+
@catalog = catalog
|
|
23
|
+
@user_config_override = user_config
|
|
24
|
+
@output = output
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def counsel
|
|
28
|
+
choices = {}
|
|
29
|
+
|
|
30
|
+
@cop_names.each do |cop_name|
|
|
31
|
+
choice = counsel_cop(cop_name)
|
|
32
|
+
if choice
|
|
33
|
+
choices[cop_name] = choice
|
|
34
|
+
yield(cop_name, choice) if block_given?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
choices
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def counsel_cop(cop_name)
|
|
44
|
+
return nil if already_configured?(cop_name)
|
|
45
|
+
|
|
46
|
+
entry = build_cop_entry(cop_name)
|
|
47
|
+
return nil unless entry
|
|
48
|
+
|
|
49
|
+
docstring = @catalog.docstring_for(cop_name)
|
|
50
|
+
|
|
51
|
+
::CLI::UI::Frame.open(entry.cop_name, color: :blue) do
|
|
52
|
+
display_cop_info(entry, docstring)
|
|
53
|
+
action = ::CLI::UI::Prompt.ask("What would you like to do?", options: ACTIONS)
|
|
54
|
+
|
|
55
|
+
case action
|
|
56
|
+
when "Disable"
|
|
57
|
+
{ "Enabled" => false }
|
|
58
|
+
when "Skip"
|
|
59
|
+
nil
|
|
60
|
+
when "Configure"
|
|
61
|
+
configure_cop(entry)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def already_configured?(cop_name)
|
|
67
|
+
user_config.key?(cop_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def user_config
|
|
71
|
+
@user_config ||= @user_config_override || load_user_config
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def load_user_config
|
|
75
|
+
return {} unless File.exist?(".rubocop.yml")
|
|
76
|
+
|
|
77
|
+
RuboCop::ConfigLoader.load_file(".rubocop.yml")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_cop_entry(cop_name)
|
|
81
|
+
cop_class = @catalog.lookup(cop_name)
|
|
82
|
+
return nil unless cop_class
|
|
83
|
+
|
|
84
|
+
examples = @catalog.examples_for(cop_name)
|
|
85
|
+
CopEntry.new(cop_class, examples: examples)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def display_cop_info(entry, docstring)
|
|
89
|
+
# Prefer full YARD docstring over short config description
|
|
90
|
+
description = docstring || entry.configuration["Description"]
|
|
91
|
+
@output.puts ::CLI::UI.fmt("{{cyan:#{description}}}\n") if description
|
|
92
|
+
|
|
93
|
+
# Show documentation URL
|
|
94
|
+
doc_url = entry.cop_class.documentation_url
|
|
95
|
+
@output.puts ::CLI::UI.fmt("{{underline:#{doc_url}}}\n") if doc_url
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def configure_cop(entry)
|
|
99
|
+
config = {}
|
|
100
|
+
|
|
101
|
+
display_array_attributes_message(entry.configurable_attributes)
|
|
102
|
+
|
|
103
|
+
entry.configurable_attributes.each do |attr|
|
|
104
|
+
value = prompt_for_attribute(attr, entry.examples)
|
|
105
|
+
config[attr.name] = value unless value.nil?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
config
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def display_array_attributes_message(attributes)
|
|
112
|
+
array_attrs = attributes
|
|
113
|
+
.select { |attr| attr.type == :array }
|
|
114
|
+
.reject { |attr| FILE_TARGETING_ATTRIBUTES.include?(attr.name) }
|
|
115
|
+
|
|
116
|
+
return if array_attrs.empty?
|
|
117
|
+
|
|
118
|
+
names = array_attrs.map(&:name).join(", ")
|
|
119
|
+
@output.puts ::CLI::UI.fmt(
|
|
120
|
+
"{{green:These attributes accept lists and can be configured directly in .rubocop.yml: #{names}}}"
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def prompt_for_attribute(attr, examples)
|
|
125
|
+
if attr.type.in?([:choice, :boolean]) && !has_examples_for?(attr.name, examples)
|
|
126
|
+
display_no_examples_message(attr.name)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
case attr.type
|
|
130
|
+
when :choice
|
|
131
|
+
prompt_for_choice(attr, examples)
|
|
132
|
+
when :boolean
|
|
133
|
+
prompt_for_boolean(attr, examples)
|
|
134
|
+
when :array
|
|
135
|
+
# Skip array attributes (Include, Exclude, AllowedMethods, etc.)
|
|
136
|
+
# These aren't style choices and prompting for them causes type corruption
|
|
137
|
+
nil
|
|
138
|
+
else
|
|
139
|
+
prompt_for_value(attr)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def prompt_for_choice(attr, examples)
|
|
144
|
+
# Put default first, then the rest
|
|
145
|
+
default_val = attr.default_value.to_s
|
|
146
|
+
sorted_options = attr.options.sort_by { |opt| opt == default_val ? 0 : 1 }
|
|
147
|
+
options_with_default = sorted_options.map do |opt|
|
|
148
|
+
opt == default_val ? "#{opt} (default)" : opt
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
loop do
|
|
152
|
+
options = has_examples_for?(attr.name, examples) ? options_with_default + ["Show examples"] : options_with_default
|
|
153
|
+
|
|
154
|
+
answer = ::CLI::UI::Prompt.ask("#{attr.name}?", options: options)
|
|
155
|
+
|
|
156
|
+
if answer == "Show examples"
|
|
157
|
+
display_examples_for_attribute(attr.name, examples)
|
|
158
|
+
else
|
|
159
|
+
# Strip "(default)" suffix if present
|
|
160
|
+
return answer.sub(/ \(default\)$/, "")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def has_examples_for?(attr_name, examples)
|
|
166
|
+
examples.any? { |ex| ex.attribute == attr_name }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def prompt_for_boolean(attr, examples)
|
|
170
|
+
# Always show default first
|
|
171
|
+
base_options = if attr.default_value
|
|
172
|
+
["yes (default)", "no"]
|
|
173
|
+
else
|
|
174
|
+
["no (default)", "yes"]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
loop do
|
|
178
|
+
options = has_examples_for?(attr.name, examples) ? base_options + ["Show examples"] : base_options
|
|
179
|
+
|
|
180
|
+
answer = ::CLI::UI::Prompt.ask("#{attr.name}?", options: options)
|
|
181
|
+
|
|
182
|
+
if answer == "Show examples"
|
|
183
|
+
display_examples_for_attribute(attr.name, examples)
|
|
184
|
+
else
|
|
185
|
+
return answer.sub(/ \(default\)$/, "") == "yes"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def prompt_for_value(attr)
|
|
191
|
+
::CLI::UI::Prompt.ask(
|
|
192
|
+
"#{attr.name}? (default: #{attr.default_value})",
|
|
193
|
+
default: attr.default_value.to_s
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def display_no_examples_message(attr_name)
|
|
198
|
+
@output.puts ::CLI::UI.fmt("{{italic:No examples available for #{attr_name}.}}")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def display_examples_for_attribute(attr_name, examples)
|
|
202
|
+
relevant = examples.select { |ex| ex.attribute == attr_name }
|
|
203
|
+
return if relevant.empty?
|
|
204
|
+
|
|
205
|
+
relevant.each do |example|
|
|
206
|
+
label = example.default? ? "#{example.value} (default)" : example.value
|
|
207
|
+
@output.puts <<~OUTPUT
|
|
208
|
+
#{::CLI::UI.fmt("{{yellow:#{label}:}}")}
|
|
209
|
+
#{example.text}
|
|
210
|
+
OUTPUT
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|