yoshiki 11.0.0.pre.10 → 11.0.0.pre.11

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: 5bf1901967e5f7d00c6463a17508751166e6981eb1edac1172e52c44c0623580
4
- data.tar.gz: 2c82f7056a5fa916ff1eff0d54c688113ff285fac49f8b7d0fe8469ebce33e70
3
+ metadata.gz: 624a862271fe20d1e371ee54c2369d19fa1ad88e447ac5a489870f6c59f5c456
4
+ data.tar.gz: 23b13cb682477e168ae3379fc5b3b8b25122c2a5ce9b11821763f8cae7aa7b7c
5
5
  SHA512:
6
- metadata.gz: ed3f86f1b4cf9bacb1ca3da1793f2b5a9fe6e0ff2b76b486600b59a81f574ef869234a7a47720be1a0f6958ef8ec47d2b947310993fb013ccb5a604fd88c87de
7
- data.tar.gz: '00294aa60e50cb7d4152902b0de1b4b22145c6fafc55f76ecbb71a7fa28aea2ba80083af30cc3708f95bcd88ded99d7a0e8066613d74c5ef91661851acb2b25c'
6
+ metadata.gz: 64ad4e0a64799205605bf6a2b3f174d9ed17018920ff5db7321cc56eafc1f8e8c4c71d17fc87f320379e0123355477f33d2805a4c6a1ad3e64be8fe4e131ecbe
7
+ data.tar.gz: e512b198eb7714d85ff507b6dfa243f10ca386a99e30d1e69aa1d988daa5c0816faf6a2bbda5c10be3c84c6f4fe2f4fb8f18629643d76c657ea5c0d307bf4266
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: write-cop
3
+ description: >
4
+ Add or change a Yoshiki RuboCop cop (custom cop, department naming,
5
+ HAML vs Ruby AST, CopHelper specs, inherit yml wiring). Use when adding
6
+ a cop, forking a stock cop, writing Yoshiki/Haml or Yoshiki/Style rules,
7
+ or when the user runs /write-cop.
8
+ ---
9
+
10
+ # Write a Yoshiki cop
11
+
12
+ Work in this gem. Apps inherit and bump; they do not grow `lib/rubocop` copies — Rails `autoload_lib` Zeitwerk-eager-loads those paths (CI especially) and blows up on `RuboCop::Cop::…`.
13
+
14
+ One cop per commit. Version bump is a later commit, not this one.
15
+
16
+ ## Name
17
+
18
+ | Kind | Name | Stock cop |
19
+ |------|------|-----------|
20
+ | Fork | `Yoshiki/<stock department>/<Cop>` | Disable it next to the Yoshiki cop |
21
+ | New Ruby | `Yoshiki/Style/<Cop>` (or Layout, Lint) | Leave stock enabled |
22
+ | Leaves Ruby (HAML tree, whole file) | `Yoshiki/Haml/<Cop>` | — |
23
+ | Shared helper | `RuboCop::Cop::Yoshiki::<Name>` module, not `< Base` | — |
24
+
25
+ Never `Yoshiki/Ruby/`. Nest under HAML (or another plugin) only when the cop is not a Ruby AST walker.
26
+
27
+ Department is the class of offense, not the syntax it touches. Silent semantic change is Lint (bare `key:` can absorb the next expression → `Lint/ParenthesizeValueOmission`). Hang/indent is Layout. Order/preference is Style.
28
+
29
+ One concern per cop. Keyword-only calls already split hang (`Layout/HangingKeywordArguments`), value-omission parens (`Lint/ParenthesizeValueOmission`), key order (`Style/SortedKeywordArguments`), padding (`Layout/HashAlignment`). Do not fold those back together.
30
+
31
+ A new rule that *feeds* a stock cop is not a fork. HashSyntax still owns `:` vs `=>`; nesting hyphenated `data`/`aria` keys is `Yoshiki/Style/NestedHtmlOptionKeys`.
32
+
33
+ ## Template
34
+
35
+ Copy the closest cop; do not invent a new shape.
36
+
37
+ | Kind | Cop | Spec |
38
+ |------|-----|------|
39
+ | Fork | `lib/rubocop/cop/yoshiki/style/it_block_parameter.rb` | `spec/rubocop/cop/yoshiki/style/it_block_parameter_spec.rb` |
40
+ | Layout / lint / style | `…/layout/hanging_keyword_arguments.rb`, `…/lint/parenthesize_value_omission.rb`, `…/style/sorted_keyword_arguments.rb` | matching `spec/…` |
41
+ | HAML document | `…/haml/sibling_blank_lines.rb` | `spec/…/haml/sibling_blank_lines_spec.rb` (`investigate_fragments`, `.haml` path) |
42
+ | Ruby hashes in HAML (extracted by `rubocop-haml`) | `…/style/nested_html_option_keys.rb` | CopHelper, no file path |
43
+ | Mixin | `lib/rubocop/cop/yoshiki/keyword_only_arguments.rb` | — |
44
+
45
+ `rubocop-haml` already feeds tag `{}` to `on_hash` / `on_send`. Walk the HAML parser only when fragments cannot see the rule (sibling blank lines).
46
+
47
+ ## Files
48
+
49
+ ```
50
+ lib/rubocop/cop/yoshiki/<dept>/<cop>.rb
51
+ spec/rubocop/cop/yoshiki/<dept>/<cop>_spec.rb
52
+ ```
53
+
54
+ Split helpers into another class in the same department (or `<cop>/*.rb`) before `Metrics/ClassLength` 100. Do not disable the metric.
55
+
56
+ Always-on Ruby: `require` in `lib/yoshiki.rb`, enable in `.yoshiki-ruby.yml`.
57
+
58
+ HAML / Rails / other plugin inherit only: `require` + `Enabled` in `.yoshiki-haml.yml` / `.yoshiki-rails.yml` (etc.), with `Include` for those files. Do not require those cops from `yoshiki.rb` — a `data:` hash in Faraday is not HTML.
59
+
60
+ Fork: disable the stock cop in the same yml (see `Style/ItBlockParameter` in `.yoshiki-ruby.yml`).
61
+
62
+ ## Specs
63
+
64
+ Ruby AST: `include CopHelper` (`spec/support/cop_helper.rb`). Define `cop_class`, `cop_name`, `cop_defaults`. **No path** on `ProcessedSource` when `autocorrect: true` — Team would write that filename in cwd.
65
+
66
+ Plugin-only cops are not loaded by `require 'yoshiki'`; `require` the cop file in the spec (see NestedHtmlOptionKeys).
67
+
68
+ HAML document cops: `investigate_fragments` with original path ending `.haml`, as SiblingBlankLines does.
69
+
70
+ Assert `corrector.rewrite` against the full expected source. Uncorrectable: offense present, source unchanged (`corrector` may be nil).
71
+
72
+ ## Autocorrect
73
+
74
+ One `corrector.replace` per overlapping range. If an ancestor rewrite already rebuilds a child node, do not also correct the child.
75
+
76
+ Pair-level edits when ranges do not overlap; rewrite the whole hash/send when keys must merge.
77
+
78
+ Leave table padding and `:` vs `=>` to HashAlignment / HashSyntax.
79
+
80
+ Collision or unsafe rewrite: `add_offense` without replacing.
81
+
82
+ When autocorrect changes call shape (parens, hang, pair order), `include ReparsedEquivalence` so a non-equivalent rewrite is dropped. Copy that from the keyword-only cops.
83
+
84
+ Do not rewrite an identifier if the replacement binds differently. `it` is per-block. Hash value omission stays `name: it`, not `it:` (`ItBlockParameter`).
85
+
86
+ ## Check
87
+
88
+ asdf Ruby from `.tool-versions` (not the app's Docker):
89
+
90
+ ```bash
91
+ asdf exec bundle exec rspec spec/rubocop/cop/yoshiki/<dept>/<cop>_spec.rb
92
+ asdf exec bundle exec rubocop -a --force-exclusion lib/rubocop/cop/yoshiki/<dept>/<cop>.rb spec/rubocop/cop/yoshiki/<dept>/<cop>_spec.rb
93
+ asdf exec bundle exec rspec
94
+ ```
95
+
96
+ Match existing cop style (`MSG = '…'.freeze`, `Style/MethodDefParentheses`). Hand-fix what `-a` leaves.
97
+
98
+ Commit message: `in which <model> & I add Yoshiki/<Dept>/<Cop>`.
data/.yoshiki-haml.yml CHANGED
@@ -3,8 +3,16 @@ plugins:
3
3
 
4
4
  require:
5
5
  - rubocop/cop/yoshiki/haml/sibling_blank_lines
6
+ - rubocop/cop/yoshiki/style/nested_html_option_keys
6
7
 
7
8
  Yoshiki/Haml/SiblingBlankLines:
8
9
  Enabled: true
9
10
  Include:
10
11
  - '**/*.haml'
12
+ Yoshiki/Style/NestedHtmlOptionKeys:
13
+ Enabled: true
14
+ Prefixes:
15
+ - data
16
+ - aria
17
+ Include:
18
+ - '**/*.haml'
data/.yoshiki-rails.yml CHANGED
@@ -1,6 +1,18 @@
1
1
  plugins:
2
2
  - rubocop-rails
3
3
 
4
+ require:
5
+ - rubocop/cop/yoshiki/style/nested_html_option_keys
6
+
7
+ Yoshiki/Style/NestedHtmlOptionKeys:
8
+ Enabled: true
9
+ Prefixes:
10
+ - data
11
+ - aria
12
+ Include:
13
+ - '**/*.haml'
14
+ - 'app/helpers/**/*.rb'
15
+
4
16
  AllCops:
5
17
  Exclude:
6
18
  - db/schema.rb
data/.yoshiki-ruby.yml CHANGED
@@ -183,6 +183,8 @@ Style/SymbolArray:
183
183
  Style/TrailingUnderscoreVariable:
184
184
  AllowNamedUnderscoreVariables: false # defaults to `true`
185
185
 
186
+ Yoshiki/Layout/EmptyLineBeforeComment:
187
+ Enabled: true
186
188
  Yoshiki/Layout/HangingKeywordArguments:
187
189
  Enabled: true
188
190
  MinKeywords: 2
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- yoshiki (11.0.0.pre.10)
4
+ yoshiki (11.0.0.pre.11)
5
5
  rubocop (= 1.89.0)
6
6
  rubocop-performance (= 1.26.1)
7
7
  rubocop-rake (= 0.7.1)
@@ -58,7 +58,7 @@ module RuboCop
58
58
 
59
59
  def expected_blanks left, right
60
60
  return 0 if packed?(left) || packed?(right)
61
- return 0 if leaf?(left) && leaf?(right)
61
+ return 0 if single_line?(left) && single_line?(right)
62
62
 
63
63
  1
64
64
  end
@@ -68,8 +68,8 @@ module RuboCop
68
68
  (node.type == :silent_script && CONTROL.include?(node.value[:keyword].to_s))
69
69
  end
70
70
 
71
- def leaf? node
72
- node.type != :filter && node.children.empty?
71
+ def single_line? node
72
+ finish(node) == node.line
73
73
  end
74
74
 
75
75
  def blank_lines left, right, lines
@@ -81,11 +81,16 @@ module RuboCop
81
81
  end
82
82
 
83
83
  def finish node
84
- extra = node.type == :filter ? node.value[:text].to_s.lines.size : 0
85
- last = (node.line || 0) + extra
84
+ last = (node.line || 0) + filter_lines(node)
86
85
  node.children.reduce(last) { |memo, child| [ memo, finish(child) ].max }
87
86
  end
88
87
 
88
+ def filter_lines node
89
+ return 0 unless node.type == :filter
90
+
91
+ node.value[:text].to_s.lines.reverse.drop_while { it.strip.empty? }.size
92
+ end
93
+
89
94
  def events_for ops
90
95
  ops.flat_map { events_for_op(it) }
91
96
  .sort_by { |lineno, kind| [ lineno, kind == :delete ? 0 : 1 ] }
@@ -111,7 +116,7 @@ module RuboCop
111
116
 
112
117
  end
113
118
 
114
- # Blank line between layout siblings; packed leaf units stay packed.
119
+ # Blank line between multi-line layout siblings; single-line siblings stay packed.
115
120
  class SiblingBlankLines < Base
116
121
 
117
122
  include RangeHelp
@@ -0,0 +1,155 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Yoshiki
4
+ module Layout
5
+ # Requires a blank line before a full-line comment group when the
6
+ # previous physical line is code at the same indent.
7
+ #
8
+ # Consecutive `#` lines and a `=begin`/`=end` document are each one
9
+ # group: one blank before the first line, nothing inserted inside.
10
+ # `=begin` is always column 0, so it still gets a blank when the
11
+ # preceding code is indented.
12
+ #
13
+ # Trailing comments stay on their code line. A `#` comment that opens a
14
+ # body (`class` / `module` / `def` / `do` / `begin` / `{`) sits deeper
15
+ # than the opener, so it is left alone. Extra indent is not a
16
+ # substitute for a blank — `Layout/CommentIndentation` will pull those
17
+ # comments back.
18
+ #
19
+ # `# rubocop:` directives glue to the code they apply to.
20
+ # `# rubocop:enable` in particular frequently trails a region and
21
+ # must not grow a blank line.
22
+ #
23
+ # @example
24
+ # # bad
25
+ # include Foo
26
+ # # associations
27
+ # has_many :bars
28
+ #
29
+ # # good
30
+ # include Foo
31
+ #
32
+ # # associations
33
+ # has_many :bars
34
+ #
35
+ # @example
36
+ # # good — first non-empty line of a body
37
+ # def join!
38
+ # # players
39
+ # seats << player
40
+ # end
41
+ #
42
+ # @example
43
+ # # good — enable trails the disabled region
44
+ # complicated_call(a, b, c)
45
+ # # rubocop:enable Metrics/MethodLength
46
+ #
47
+ # @example
48
+ # # good — disable glues to the following statement
49
+ # simple_call
50
+ # # rubocop:disable Metrics/MethodLength
51
+ # complicated_call(a, b, c)
52
+ #
53
+ # @example
54
+ # # bad
55
+ # include Foo
56
+ # =begin
57
+ # associations
58
+ # =end
59
+ # has_many :bars
60
+ #
61
+ # # good
62
+ # include Foo
63
+ #
64
+ # =begin
65
+ # associations
66
+ # =end
67
+ # has_many :bars
68
+ #
69
+ class EmptyLineBeforeComment < Base
70
+
71
+ include RangeHelp
72
+ extend AutoCorrector
73
+
74
+ MSG = 'Add a blank line before this comment.'.freeze
75
+
76
+ def on_new_investigation
77
+ processed_source.comments.each { inspect_comment(it) }
78
+ end
79
+
80
+ private
81
+
82
+ def inspect_comment comment
83
+ return unless own_line_comment?(comment)
84
+ return if skip_comment?(comment)
85
+ return unless preceded_by_code?(comment)
86
+
87
+ add_offense(comment) { autocorrect(it, comment) }
88
+ end
89
+
90
+ def autocorrect corrector, comment
91
+ corrector.insert_before(line_begin(comment.loc.line), "\n")
92
+ end
93
+
94
+ def skip_comment? comment
95
+ magic_comment?(comment) || directive_comment?(comment)
96
+ end
97
+
98
+ def preceded_by_code? comment
99
+ previous = previous_line(comment)
100
+ return false unless previous
101
+ return false if blank?(previous)
102
+ return false if comment_line?(previous)
103
+ return true if comment.document?
104
+ return false if body_opener?(previous)
105
+
106
+ indent(previous) == indent(line_at(comment.loc.line))
107
+ end
108
+
109
+ def own_line_comment? comment
110
+ comment.document? || comment_line?(line_at(comment.loc.line))
111
+ end
112
+
113
+ def magic_comment? comment
114
+ MagicComment.parse(comment.text).any?
115
+ end
116
+
117
+ def directive_comment? comment
118
+ DirectiveComment.new(comment).start_with_marker?
119
+ end
120
+
121
+ def previous_line comment
122
+ return if comment.loc.line <= 1
123
+
124
+ line_at(comment.loc.line - 1)
125
+ end
126
+
127
+ def line_at lineno
128
+ processed_source.lines[lineno - 1]
129
+ end
130
+
131
+ def line_begin lineno
132
+ source_range(processed_source.buffer, lineno, 0)
133
+ end
134
+
135
+ def blank? line
136
+ line.strip.empty?
137
+ end
138
+
139
+ def comment_line? line
140
+ /\A\s*#/.match?(line) || /\A=begin\b/.match?(line) || /\A=end\b/.match?(line)
141
+ end
142
+
143
+ def body_opener? line
144
+ /\A\s*(else|elsif|when|in|rescue|ensure)\b/.match?(line)
145
+ end
146
+
147
+ def indent line
148
+ line[/\A[ \t]*/].length
149
+ end
150
+
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,281 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Yoshiki
4
+ module Style
5
+ class NestedHtmlOptionKeys < Base
6
+
7
+ # Rebuild a hash with hyphenated data/aria keys nested.
8
+ class Rewriter
9
+
10
+ def initialize node, prefixes
11
+ @node = node
12
+ @forest = Forest.new(node, prefixes)
13
+ @entries = @forest.entries
14
+ end
15
+
16
+ def offenses
17
+ changed? ? @forest.all_offenses : @forest.direct
18
+ end
19
+
20
+ def changed?
21
+ @forest.direct.any? && replacement != @node.source
22
+ end
23
+
24
+ def replacement
25
+ @replacement ||= Printer.new.render(@node, @entries)
26
+ end
27
+
28
+ end
29
+
30
+ # Walk pairs into a nestable entry list.
31
+ class Forest
32
+
33
+ attr_reader :entries, :direct, :all_offenses
34
+
35
+ def initialize node, prefixes
36
+ @prefixes = prefixes
37
+ @direct = []
38
+ @all_offenses = []
39
+ @tree = Tree.new(self)
40
+ @entries = build(node, mode_for(node), true)
41
+ end
42
+
43
+ def build node, mode, direct
44
+ node.children.each_with_object([]) do |child, entries|
45
+ push(entries, child, mode, direct)
46
+ end
47
+ end
48
+
49
+ def prefix? name
50
+ name && @prefixes.include?(name)
51
+ end
52
+
53
+ def note_offense pair, direct
54
+ range = pair.key.source_range
55
+ @all_offenses << range
56
+ @direct << range if direct
57
+ end
58
+
59
+ private
60
+
61
+ def mode_for node
62
+ pair = node.parent
63
+ return :inside_prefix if pair&.pair_type? && prefix?(key_name(pair))
64
+
65
+ :html_options
66
+ end
67
+
68
+ def push entries, child, mode, direct
69
+ name = child.pair_type? ? key_name(child) : nil
70
+ return @tree.append(entries, :raw, source: child.source) unless name
71
+
72
+ path = nest_path(name, mode)
73
+ return @tree.insert_path(entries, path, child, direct) if path
74
+ return @tree.merge_hash(entries, name, child, mode) if nestable_hash?(child, name, mode)
75
+
76
+ @tree.append(entries, :raw, name: name, source: child.source)
77
+ end
78
+
79
+ def nestable_hash? child, name, mode
80
+ child.value.hash_type? && (mode == :inside_prefix || prefix?(name))
81
+ end
82
+
83
+ def nest_path name, mode
84
+ return name.split('-') if mode == :inside_prefix && hyphenated_idents?(name)
85
+
86
+ prefixed_path(name) unless mode == :inside_prefix
87
+ end
88
+
89
+ def prefixed_path name
90
+ prefix, rest = split_prefixed(name)
91
+ return unless prefix && rest
92
+ return unless rest.split('-').all? { NestedHtmlOptionKeys::IDENT.match?(it) }
93
+
94
+ [ prefix ] + rest.split('-')
95
+ end
96
+
97
+ def split_prefixed name
98
+ @prefixes.each do |prefix|
99
+ next unless name.start_with?("#{ prefix }-")
100
+
101
+ return [ prefix, name.delete_prefix("#{ prefix }-") ]
102
+ end
103
+ nil
104
+ end
105
+
106
+ def hyphenated_idents? name
107
+ name.include?('-') && name.split('-').all? { NestedHtmlOptionKeys::IDENT.match?(it) }
108
+ end
109
+
110
+ def key_name pair
111
+ return unless pair&.pair_type?
112
+
113
+ key = pair.key
114
+ return key.value.to_s if key.sym_type?
115
+ return key.value if key.str_type?
116
+
117
+ nil
118
+ end
119
+
120
+ end
121
+
122
+ # Mutate an entry list: nest, merge, or keep the original pair.
123
+ class Tree
124
+
125
+ def initialize forest
126
+ @forest = forest
127
+ end
128
+
129
+ def insert_path entries, path, pair, direct
130
+ @forest.note_offense(pair, direct)
131
+ slot = ensure_branch(entries, path.first)
132
+ return if slot && merge_leaf?(slot[:children], path.drop(1), pair)
133
+
134
+ entries.delete(slot) if slot && slot[:children].empty?
135
+ append(entries, :raw, source: pair.source)
136
+ end
137
+
138
+ def merge_hash entries, name, pair, mode
139
+ nested = @forest.build(pair.value, (@forest.prefix?(name) ? :inside_prefix : mode), false)
140
+ slot = find_named(entries, name)
141
+ return append(entries, :branch, name: name, children: nested, node: pair.value) if slot.nil?
142
+ return absorb(slot, nested, pair.value) if slot[:kind] == :branch
143
+
144
+ append(entries, :raw, source: pair.source)
145
+ end
146
+
147
+ def append entries, kind, **fields
148
+ entry = { kind: kind, **fields }
149
+ entries << entry
150
+ entry
151
+ end
152
+
153
+ private
154
+
155
+ def ensure_branch entries, name
156
+ slot = find_named(entries, name)
157
+ return append(entries, :branch, name: name, children: []) if slot.nil?
158
+ return slot if slot[:kind] == :branch
159
+
160
+ nil
161
+ end
162
+
163
+ def merge_leaf? entries, path, pair
164
+ first, *rest = path
165
+ return add_leaf?(entries, first, pair) if rest.empty?
166
+
167
+ slot = ensure_branch(entries, first) or return false
168
+ return true if merge_leaf?(slot[:children], rest, pair)
169
+
170
+ entries.delete(slot) if slot[:children].empty?
171
+ false
172
+ end
173
+
174
+ def add_leaf? entries, name, pair
175
+ return false if find_named(entries, name)
176
+
177
+ source = pair.value_omission? ? '' : pair.value.source
178
+ append(entries, :leaf, name: name, source: source)
179
+ true
180
+ end
181
+
182
+ def absorb slot, nested, hash
183
+ slot[:node] ||= hash
184
+ nested.each { merge_entry(slot[:children], it) }
185
+ end
186
+
187
+ def merge_entry entries, entry
188
+ if (existing = mergeable_branch(entries, entry))
189
+ (entry[:children] || []).each { merge_entry(existing[:children], it) }
190
+ elsif entry[:kind] != :raw && find_named(entries, entry[:name])
191
+ append(entries, :raw, source: entry[:source] || Printer.new.render_entry(entry))
192
+ else
193
+ entries << entry
194
+ end
195
+ end
196
+
197
+ def mergeable_branch entries, entry
198
+ return if entry[:kind] != :branch
199
+
200
+ existing = find_named(entries, entry[:name])
201
+ existing if existing && existing[:kind] == :branch
202
+ end
203
+
204
+ def find_named entries, name
205
+ entries.find { it[:name] == name }
206
+ end
207
+
208
+ end
209
+
210
+ # Format nested entries back into a hash literal.
211
+ class Printer
212
+
213
+ def render node, entries
214
+ parts = entries.map { render_entry(it) }
215
+ return multiline(node, parts) if node.multiline?
216
+ return "{ #{ parts.join(', ') } }" if node.braces?
217
+
218
+ parts.join(', ')
219
+ end
220
+
221
+ def render_entry entry
222
+ case entry[:kind]
223
+ when :raw then entry[:source]
224
+ when :leaf then leaf_source(entry)
225
+ else "#{ entry[:name] }: #{ render_branch(entry) }"
226
+ end
227
+ end
228
+
229
+ private
230
+
231
+ def multiline node, parts
232
+ indent = ' ' * continuation_column(node)
233
+ return parts.join(",\n#{ indent }") unless node.braces?
234
+
235
+ braced_multiline(node, parts, indent)
236
+ end
237
+
238
+ def braced_multiline node, parts, indent
239
+ if node.children.first.source_range.line == node.loc.begin.line
240
+ same_line_brace(parts, indent)
241
+ else
242
+ close = ' ' * node.loc.end.column
243
+ "{\n#{ indent }#{ parts.join(",\n#{ indent }") }\n#{ close }}"
244
+ end
245
+ end
246
+
247
+ def same_line_brace parts, indent
248
+ rest = parts.drop(1)
249
+ return "{ #{ parts.first } }" if rest.empty?
250
+
251
+ "{ #{ parts.first },\n#{ indent }#{ rest.join(",\n#{ indent }") } }"
252
+ end
253
+
254
+ def continuation_column node
255
+ children = node.children
256
+ rest = children[1]
257
+ if rest && rest.source_range.line != children.first.source_range.line
258
+ rest.source_range.column
259
+ else
260
+ children.first.source_range.column
261
+ end
262
+ end
263
+
264
+ def render_branch entry
265
+ children = entry[:children] || []
266
+ return render(entry[:node], children) if entry[:node]
267
+
268
+ "{ #{ children.map { render_entry(it) }.join(', ') } }"
269
+ end
270
+
271
+ def leaf_source entry
272
+ entry[:source].empty? ? "#{ entry[:name] }:" : "#{ entry[:name] }: #{ entry[:source] }"
273
+ end
274
+
275
+ end
276
+
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,76 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Yoshiki
4
+ module Style
5
+ # Nests hyphenated `data-*` / `aria-*` keys so every key is a 1.9
6
+ # identifier. Rails and Haml flatten the nested hashes back to the
7
+ # same HTML (`data-game-live-target`, `aria-label`).
8
+ #
9
+ # This is not a HashSyntax replacement. HashSyntax still owns `:` vs
10
+ # `=>`. Nesting first is what lets HashSyntax stay in ruby19 form
11
+ # instead of mixed rockets or quoted-symbol keys.
12
+ #
13
+ # Keys whose hyphen segments are not identifiers (`1p-ignore`) stay.
14
+ # Real HTML attributes (`http-equiv`) stay. Stimulus controller names
15
+ # are not renamed — hyphens split in place.
16
+ #
17
+ # @example
18
+ # # bad
19
+ # { data: { controller: 'chat', 'chat-target' => 'input' } }
20
+ # { 'aria-label' => placeholder }
21
+ #
22
+ # # good
23
+ # { data: { controller: 'chat', chat: { target: 'input' } } }
24
+ # { aria: { label: placeholder } }
25
+ class NestedHtmlOptionKeys < Base
26
+
27
+ extend AutoCorrector
28
+
29
+ MSG = 'Nest hyphenated HTML option keys as symbol hashes.'.freeze
30
+ IDENT = /\A[a-z_][a-z0-9_]*\z/
31
+
32
+ def on_hash node
33
+ (@hashes ||= []) << node
34
+ end
35
+
36
+ def on_investigation_end
37
+ flagged.each { register(it) }
38
+ @hashes = []
39
+
40
+ super
41
+ end
42
+
43
+ private
44
+
45
+ def prefixes
46
+ cop_config.fetch('Prefixes', %w[ data aria ])
47
+ end
48
+
49
+ def flagged
50
+ hashes = (@hashes || []).select { rewriter(it).offenses.any? }
51
+ hashes.reject! { it.each_ancestor(:hash).any? { rewriter(it).changed? } }
52
+ hashes
53
+ end
54
+
55
+ def rewriter node
56
+ Rewriter.new(node, prefixes)
57
+ end
58
+
59
+ def register node
60
+ built = rewriter(node)
61
+ built.offenses.each_with_index do |range, index|
62
+ add_offense(range) do |corrector|
63
+ next unless index.zero? && built.changed?
64
+
65
+ corrector.replace(node, built.replacement)
66
+ end
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ require_relative 'nested_html_option_keys/rewriter'
@@ -1,5 +1,5 @@
1
1
  module Yoshiki
2
2
 
3
- VERSION = '11.0.0-10'.freeze
3
+ VERSION = '11.0.0-11'.freeze
4
4
 
5
5
  end
data/lib/yoshiki.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'yoshiki/version'
2
2
  require 'rubocop'
3
3
  require 'rubocop/cop/yoshiki/keyword_only_arguments'
4
+ require 'rubocop/cop/yoshiki/layout/empty_line_before_comment'
4
5
  require 'rubocop/cop/yoshiki/layout/hanging_keyword_arguments'
5
6
  require 'rubocop/cop/yoshiki/lint/parenthesize_value_omission'
6
7
  require 'rubocop/cop/yoshiki/style/it_block_parameter'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yoshiki
3
3
  version: !ruby/object:Gem::Version
4
- version: 11.0.0.pre.10
4
+ version: 11.0.0.pre.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - BM5k
@@ -61,6 +61,7 @@ files:
61
61
  - ".git-blame-ignore-revs"
62
62
  - ".gitignore"
63
63
  - ".gitlab-ci.yml"
64
+ - ".grok/skills/write-cop/SKILL.md"
64
65
  - ".rspec"
65
66
  - ".rubocop-defaults-capybara.yml"
66
67
  - ".rubocop-defaults-minitest.yml"
@@ -85,9 +86,12 @@ files:
85
86
  - bin/console
86
87
  - lib/rubocop/cop/yoshiki/haml/sibling_blank_lines.rb
87
88
  - lib/rubocop/cop/yoshiki/keyword_only_arguments.rb
89
+ - lib/rubocop/cop/yoshiki/layout/empty_line_before_comment.rb
88
90
  - lib/rubocop/cop/yoshiki/layout/hanging_keyword_arguments.rb
89
91
  - lib/rubocop/cop/yoshiki/lint/parenthesize_value_omission.rb
90
92
  - lib/rubocop/cop/yoshiki/style/it_block_parameter.rb
93
+ - lib/rubocop/cop/yoshiki/style/nested_html_option_keys.rb
94
+ - lib/rubocop/cop/yoshiki/style/nested_html_option_keys/rewriter.rb
91
95
  - lib/rubocop/cop/yoshiki/style/sorted_keyword_arguments.rb
92
96
  - lib/yoshiki.rb
93
97
  - lib/yoshiki/examples.rb