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 +4 -4
- data/.grok/skills/write-cop/SKILL.md +98 -0
- data/.yoshiki-haml.yml +8 -0
- data/.yoshiki-rails.yml +12 -0
- data/.yoshiki-ruby.yml +2 -0
- data/Gemfile.lock +1 -1
- data/lib/rubocop/cop/yoshiki/haml/sibling_blank_lines.rb +11 -6
- data/lib/rubocop/cop/yoshiki/layout/empty_line_before_comment.rb +155 -0
- data/lib/rubocop/cop/yoshiki/style/nested_html_option_keys/rewriter.rb +281 -0
- data/lib/rubocop/cop/yoshiki/style/nested_html_option_keys.rb +76 -0
- data/lib/yoshiki/version.rb +1 -1
- data/lib/yoshiki.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 624a862271fe20d1e371ee54c2369d19fa1ad88e447ac5a489870f6c59f5c456
|
|
4
|
+
data.tar.gz: 23b13cb682477e168ae3379fc5b3b8b25122c2a5ce9b11821763f8cae7aa7b7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
|
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
|
|
72
|
-
node
|
|
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
|
-
|
|
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;
|
|
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'
|
data/lib/yoshiki/version.rb
CHANGED
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.
|
|
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
|