rubocounsel 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df8f7f81d3443e7d62fbf3dc59fb31d2f0e491c93fec1b7c9e6d8150266d7517
4
- data.tar.gz: ea8ffb9483013e9194dd188bc6300ffe5f0009f1cced90330f8099987925a9df
3
+ metadata.gz: ae3bff325e5426cf4c5fc317a6115e4564fcfa6b2c9623bb6049dbc097036a48
4
+ data.tar.gz: d07dd7dd339759d409bfa47410e2adb432f0ccece654177181891af77714d6c8
5
5
  SHA512:
6
- metadata.gz: cb5255b7e2f10bb3ed885a1db2e21abff41ef12b90dd553b79a9f153b67abab42de64e3dc20d629c9a64fcc95b493e67d6d9e7e584ccf60f981ec1fdf49b1e51
7
- data.tar.gz: c11acd191699d9d282e13120277fa125b9b5d02e714b97f54ff7d02495c913faef843d208ff700e2dab9fa3190b52e77821c526247372b41d2e4404e4ac5f7f4
6
+ metadata.gz: 0d0e95026cdd70567ea9ee806eaa6d50be180313846c35afe7ffd5d95be2fc9df03ce70f187a4c6359ed000945703a59e2984c850298b27bde8b5c7738d084e7
7
+ data.tar.gz: cae2f648bd814c0e86432f703e5fa44b67c71c860e279d6e94bd8501fb61e74e15aaeb4f4a5fe3b156849b645d0c2b320632dbf3e5a279249edb203efc886f8b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.2] - 2026-03-22
4
+
5
+ - Show "Show examples" option for cops with no configurable attributes
6
+ - Refactor to polymorphic ConfigurableAttribute hierarchy with .for() factory
7
+ - Extract composable CopActions and CopPrompt classes
8
+
3
9
  ## [0.1.1] - 2026-03-21
4
10
 
5
11
  - Show "Enable" action instead of "Configure" for cops with no configurable attributes
@@ -1,44 +1,116 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cli/ui"
4
+
3
5
  module RuboCounsel
4
6
  # Represents one configurable aspect of a cop (e.g., EnforcedStyle, Max).
5
7
  #
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
8
+ # Use the .for factory to get the right subclass based on the value type:
9
+ #
10
+ # ConfigurableAttribute.for("EnforcedStyle", "single_quotes", options: [...])
11
+ # # => ChoiceAttribute
12
+ #
13
+ # Each subclass knows how to prompt for its own value via #prompt.
12
14
  class ConfigurableAttribute
13
- attr_reader :name, :default_value, :options
15
+ attr_reader :name, :default_value, :options, :examples
16
+
17
+ def self.for(name, value, options: nil, examples: [])
18
+ if options
19
+ ChoiceAttribute.new(name, value, options: options, examples: examples)
20
+ elsif value.is_a?(Array)
21
+ ArrayAttribute.new(name, value, examples: examples)
22
+ elsif value.in?([true, false])
23
+ BooleanAttribute.new(name, value, examples: examples)
24
+ else
25
+ ScalarAttribute.new(name, value, examples: examples)
26
+ end
27
+ end
14
28
 
15
- def initialize(name, default_value, options: nil)
29
+ def initialize(name, default_value, options: nil, examples: [])
16
30
  @name = name
17
31
  @default_value = default_value
18
32
  @options = options
33
+ @examples = examples
19
34
  end
20
35
 
21
- def type
22
- return :choice if @options
36
+ def promptable? = true
37
+ def choice? = false
38
+ def boolean? = false
23
39
 
24
- case @default_value
25
- when true, false
26
- :boolean
27
- when Integer
28
- :integer
29
- when Array
30
- :array
31
- else
32
- :string
40
+ def has_examples?
41
+ @examples.any?
42
+ end
43
+
44
+ # Template method: prompt with "Show examples" re-prompt loop.
45
+ # Subclasses provide #base_options and #parse_answer.
46
+ def prompt(output: $stdout)
47
+ output.puts ::CLI::UI.fmt("{{italic:No examples available for #{@name}.}}") unless has_examples?
48
+
49
+ loop do
50
+ options = has_examples? ? base_options + ["Show examples"] : base_options
51
+ answer = ::CLI::UI::Prompt.ask("#{@name}?", options: options)
52
+
53
+ if answer == "Show examples"
54
+ display_examples(output)
55
+ else
56
+ return parse_answer(answer)
57
+ end
33
58
  end
34
59
  end
35
60
 
36
- def choice?
37
- type == :choice
61
+ private
62
+
63
+ def display_examples(output)
64
+ @examples.each do |example|
65
+ label = example.default? ? "#{example.value} (default)" : example.value
66
+ output.puts <<~OUTPUT
67
+ #{::CLI::UI.fmt("{{yellow:#{label}:}}")}
68
+ #{example.text}
69
+ OUTPUT
70
+ end
38
71
  end
72
+ end
73
+
74
+ class ChoiceAttribute < ConfigurableAttribute
75
+ def choice? = true
39
76
 
40
- def boolean?
41
- type == :boolean
77
+ private
78
+
79
+ def base_options
80
+ default_val = @default_value.to_s
81
+ sorted = @options.sort_by { |opt| opt == default_val ? 0 : 1 }
82
+ sorted.map { |opt| opt == default_val ? "#{opt} (default)" : opt }
83
+ end
84
+
85
+ def parse_answer(answer)
86
+ answer.sub(/ \(default\)$/, "")
87
+ end
88
+ end
89
+
90
+ class BooleanAttribute < ConfigurableAttribute
91
+ def boolean? = true
92
+
93
+ private
94
+
95
+ def base_options
96
+ @default_value ? ["yes (default)", "no"] : ["no (default)", "yes"]
42
97
  end
98
+
99
+ def parse_answer(answer)
100
+ answer.sub(/ \(default\)$/, "") == "yes"
101
+ end
102
+ end
103
+
104
+ class ScalarAttribute < ConfigurableAttribute
105
+ def prompt(output: $stdout)
106
+ ::CLI::UI::Prompt.ask(
107
+ "#{@name}? (default: #{@default_value})",
108
+ default: @default_value.to_s
109
+ )
110
+ end
111
+ end
112
+
113
+ class ArrayAttribute < ConfigurableAttribute
114
+ def promptable? = false
43
115
  end
44
116
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCounsel
4
+ module CopActions
5
+ class Enable
6
+ def label = "Enable"
7
+ def reprompt? = false
8
+ def call = { "Enabled" => true }
9
+ end
10
+
11
+ class Disable
12
+ def label = "Disable"
13
+ def reprompt? = false
14
+ def call = { "Enabled" => false }
15
+ end
16
+
17
+ class Skip
18
+ def label = "Skip"
19
+ def reprompt? = false
20
+ def call = nil
21
+ end
22
+
23
+ # Displays all examples for a cop, then signals re-prompt.
24
+ class ShowExamples
25
+ def initialize(examples, output: $stdout)
26
+ @examples = examples
27
+ @output = output
28
+ end
29
+
30
+ def label = "Show examples"
31
+ def reprompt? = true
32
+
33
+ def call
34
+ @examples.each do |example|
35
+ example_label = example.default? ? "#{example.value} (default)" : example.value
36
+ @output.puts <<~OUTPUT
37
+ #{::CLI::UI.fmt("{{yellow:#{example_label}:}}")}
38
+ #{example.text}
39
+ OUTPUT
40
+ end
41
+ end
42
+ end
43
+
44
+ # Walks through each promptable attribute and collects configuration.
45
+ class Configure
46
+ FILE_TARGETING_ATTRIBUTES = ["Include", "Exclude"].freeze
47
+
48
+ def initialize(entry, output: $stdout)
49
+ @entry = entry
50
+ @output = output
51
+ end
52
+
53
+ def label = "Configure"
54
+ def reprompt? = false
55
+
56
+ def call
57
+ display_array_attributes_message
58
+
59
+ config = {}
60
+ @entry.configurable_attributes.select(&:promptable?).each do |attr|
61
+ value = attr.prompt(output: @output)
62
+ config[attr.name] = value unless value.nil?
63
+ end
64
+ config
65
+ end
66
+
67
+ private
68
+
69
+ def display_array_attributes_message
70
+ array_names = @entry.configurable_attributes
71
+ .reject(&:promptable?)
72
+ .reject { |attr| FILE_TARGETING_ATTRIBUTES.include?(attr.name) }
73
+ .map(&:name)
74
+
75
+ return if array_names.empty?
76
+
77
+ @output.puts ::CLI::UI.fmt(
78
+ "{{green:These attributes accept lists and can be configured directly in .rubocop.yml: #{array_names.join(", ")}}}"
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
@@ -43,20 +43,21 @@ module RuboCounsel
43
43
  @configurable_attributes ||= build_configurable_attributes
44
44
  end
45
45
 
46
+ def configurable?
47
+ configurable_attributes.any?(&:promptable?)
48
+ end
49
+
46
50
  private
47
51
 
48
52
  def build_configurable_attributes
49
- attributes = []
50
-
51
- configuration.each do |key, value|
53
+ configuration.each_with_object([]) do |(key, value), attributes|
52
54
  next if metadata_key?(key)
53
55
  next if supported_key?(key)
54
56
 
55
57
  options = find_options_for(key)
56
- attributes << ConfigurableAttribute.new(key, value, options: options)
58
+ attr_examples = @examples.select { |ex| ex.attribute == key }
59
+ attributes << ConfigurableAttribute.for(key, value, options: options, examples: attr_examples)
57
60
  end
58
-
59
- attributes
60
61
  end
61
62
 
62
63
  def metadata_key?(key)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cli/ui"
4
+
5
+ module RuboCounsel
6
+ # Presents a list of actions for a cop and collects the user's choice.
7
+ #
8
+ # Actions are composable objects with #label, #call, and #reprompt?.
9
+ # The prompt loops until an action returns a result (reprompt? is false).
10
+ class CopPrompt
11
+ def self.for(entry, output: $stdout)
12
+ if entry.configurable?
13
+ actions = [
14
+ CopActions::Configure.new(entry, output: output),
15
+ CopActions::Disable.new,
16
+ CopActions::Skip.new
17
+ ]
18
+ else
19
+ actions = []
20
+ actions << CopActions::ShowExamples.new(entry.examples, output: output) if entry.examples.any?
21
+ actions.push(CopActions::Enable.new, CopActions::Disable.new, CopActions::Skip.new)
22
+ end
23
+
24
+ new(actions: actions)
25
+ end
26
+
27
+ def initialize(actions:)
28
+ @actions = actions
29
+ end
30
+
31
+ def prompt
32
+ loop do
33
+ labels = @actions.map(&:label)
34
+ chosen = ::CLI::UI::Prompt.ask("What would you like to do?", options: labels)
35
+ action = @actions.find { |a| a.label == chosen }
36
+ result = action.call
37
+ return result unless action.reprompt?
38
+ end
39
+ end
40
+ end
41
+ end
@@ -8,16 +8,12 @@ module RuboCounsel
8
8
  #
9
9
  # Responsibilities:
10
10
  # - Walk through each offending cop
11
- # - Display cop information and examples
12
- # - Collect user choices via cli-ui prompts
11
+ # - Display cop information
12
+ # - Delegate action prompting to CopPrompt
13
13
  # - Return choices hash for ConfigWriter
14
14
  #
15
15
  # Cops that are already configured in .rubocop.yml are skipped by default.
16
16
  class Counselor
17
- CONFIGURABLE_ACTIONS = ["Configure", "Disable", "Skip"].freeze
18
- SIMPLE_ACTIONS = ["Enable", "Disable", "Skip"].freeze
19
- FILE_TARGETING_ATTRIBUTES = ["Include", "Exclude"].freeze
20
-
21
17
  def initialize(cop_names, catalog: CopCatalog.new, user_config: nil, output: $stdout)
22
18
  @cop_names = cop_names
23
19
  @catalog = catalog
@@ -51,19 +47,7 @@ module RuboCounsel
51
47
 
52
48
  ::CLI::UI::Frame.open(entry.cop_name, color: :blue) do
53
49
  display_cop_info(entry, docstring)
54
- actions = entry.configurable_attributes.empty? ? SIMPLE_ACTIONS : CONFIGURABLE_ACTIONS
55
- action = ::CLI::UI::Prompt.ask("What would you like to do?", options: actions)
56
-
57
- case action
58
- when "Enable"
59
- { "Enabled" => true }
60
- when "Disable"
61
- { "Enabled" => false }
62
- when "Skip"
63
- nil
64
- when "Configure"
65
- configure_cop(entry)
66
- end
50
+ CopPrompt.for(entry, output: @output).prompt
67
51
  end
68
52
  end
69
53
 
@@ -90,129 +74,11 @@ module RuboCounsel
90
74
  end
91
75
 
92
76
  def display_cop_info(entry, docstring)
93
- # Prefer full YARD docstring over short config description
94
77
  description = docstring || entry.configuration["Description"]
95
78
  @output.puts ::CLI::UI.fmt("{{cyan:#{description}}}\n") if description
96
79
 
97
- # Show documentation URL
98
80
  doc_url = entry.cop_class.documentation_url
99
81
  @output.puts ::CLI::UI.fmt("{{underline:#{doc_url}}}\n") if doc_url
100
82
  end
101
-
102
- def configure_cop(entry)
103
- config = {}
104
-
105
- display_array_attributes_message(entry.configurable_attributes)
106
-
107
- entry.configurable_attributes.each do |attr|
108
- value = prompt_for_attribute(attr, entry.examples)
109
- config[attr.name] = value unless value.nil?
110
- end
111
-
112
- config
113
- end
114
-
115
- def display_array_attributes_message(attributes)
116
- array_attrs = attributes
117
- .select { |attr| attr.type == :array }
118
- .reject { |attr| FILE_TARGETING_ATTRIBUTES.include?(attr.name) }
119
-
120
- return if array_attrs.empty?
121
-
122
- names = array_attrs.map(&:name).join(", ")
123
- @output.puts ::CLI::UI.fmt(
124
- "{{green:These attributes accept lists and can be configured directly in .rubocop.yml: #{names}}}"
125
- )
126
- end
127
-
128
- def prompt_for_attribute(attr, examples)
129
- if attr.type.in?([:choice, :boolean]) && !has_examples_for?(attr.name, examples)
130
- display_no_examples_message(attr.name)
131
- end
132
-
133
- case attr.type
134
- when :choice
135
- prompt_for_choice(attr, examples)
136
- when :boolean
137
- prompt_for_boolean(attr, examples)
138
- when :array
139
- # Skip array attributes (Include, Exclude, AllowedMethods, etc.)
140
- # These aren't style choices and prompting for them causes type corruption
141
- nil
142
- else
143
- prompt_for_value(attr)
144
- end
145
- end
146
-
147
- def prompt_for_choice(attr, examples)
148
- # Put default first, then the rest
149
- default_val = attr.default_value.to_s
150
- sorted_options = attr.options.sort_by { |opt| opt == default_val ? 0 : 1 }
151
- options_with_default = sorted_options.map do |opt|
152
- opt == default_val ? "#{opt} (default)" : opt
153
- end
154
-
155
- loop do
156
- options = has_examples_for?(attr.name, examples) ? options_with_default + ["Show examples"] : options_with_default
157
-
158
- answer = ::CLI::UI::Prompt.ask("#{attr.name}?", options: options)
159
-
160
- if answer == "Show examples"
161
- display_examples_for_attribute(attr.name, examples)
162
- else
163
- # Strip "(default)" suffix if present
164
- return answer.sub(/ \(default\)$/, "")
165
- end
166
- end
167
- end
168
-
169
- def has_examples_for?(attr_name, examples)
170
- examples.any? { |ex| ex.attribute == attr_name }
171
- end
172
-
173
- def prompt_for_boolean(attr, examples)
174
- # Always show default first
175
- base_options = if attr.default_value
176
- ["yes (default)", "no"]
177
- else
178
- ["no (default)", "yes"]
179
- end
180
-
181
- loop do
182
- options = has_examples_for?(attr.name, examples) ? base_options + ["Show examples"] : base_options
183
-
184
- answer = ::CLI::UI::Prompt.ask("#{attr.name}?", options: options)
185
-
186
- if answer == "Show examples"
187
- display_examples_for_attribute(attr.name, examples)
188
- else
189
- return answer.sub(/ \(default\)$/, "") == "yes"
190
- end
191
- end
192
- end
193
-
194
- def prompt_for_value(attr)
195
- ::CLI::UI::Prompt.ask(
196
- "#{attr.name}? (default: #{attr.default_value})",
197
- default: attr.default_value.to_s
198
- )
199
- end
200
-
201
- def display_no_examples_message(attr_name)
202
- @output.puts ::CLI::UI.fmt("{{italic:No examples available for #{attr_name}.}}")
203
- end
204
-
205
- def display_examples_for_attribute(attr_name, examples)
206
- relevant = examples.select { |ex| ex.attribute == attr_name }
207
- return if relevant.empty?
208
-
209
- relevant.each do |example|
210
- label = example.default? ? "#{example.value} (default)" : example.value
211
- @output.puts <<~OUTPUT
212
- #{::CLI::UI.fmt("{{yellow:#{label}:}}")}
213
- #{example.text}
214
- OUTPUT
215
- end
216
- end
217
83
  end
218
84
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuboCounsel
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/rubocounsel.rb CHANGED
@@ -6,6 +6,8 @@ require_relative "rubocounsel/example_loader"
6
6
  require_relative "rubocounsel/configurable_attribute"
7
7
  require_relative "rubocounsel/cop_entry"
8
8
  require_relative "rubocounsel/cop_catalog"
9
+ require_relative "rubocounsel/cop_actions"
10
+ require_relative "rubocounsel/cop_prompt"
9
11
  require_relative "rubocounsel/config_writer"
10
12
  require_relative "rubocounsel/counselor"
11
13
  require_relative "rubocounsel/runner"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocounsel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John DeWyze
@@ -90,8 +90,10 @@ files:
90
90
  - lib/rubocounsel/cli.rb
91
91
  - lib/rubocounsel/config_writer.rb
92
92
  - lib/rubocounsel/configurable_attribute.rb
93
+ - lib/rubocounsel/cop_actions.rb
93
94
  - lib/rubocounsel/cop_catalog.rb
94
95
  - lib/rubocounsel/cop_entry.rb
96
+ - lib/rubocounsel/cop_prompt.rb
95
97
  - lib/rubocounsel/counselor.rb
96
98
  - lib/rubocounsel/example.rb
97
99
  - lib/rubocounsel/example_loader.rb