yoshiki 11.0.0.pre.7 → 11.0.0.pre.8
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/.rubocop-defaults-rails.yml +2 -0
- data/.rubocop-defaults.yml +0 -2
- data/.yoshiki-haml.yml +10 -0
- data/.yoshiki-rails.yml +4 -2
- data/.yoshiki-ruby.yml +20 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +17 -1
- data/README.md +13 -0
- data/lib/rubocop/cop/yoshiki/haml/sibling_blank_lines.rb +165 -0
- data/lib/rubocop/cop/yoshiki/keyword_only_arguments.rb +76 -0
- data/lib/rubocop/cop/yoshiki/layout/hanging_keyword_arguments.rb +135 -0
- data/lib/rubocop/cop/yoshiki/lint/parenthesize_value_omission.rb +90 -0
- data/lib/rubocop/cop/yoshiki/style/it_block_parameter.rb +85 -0
- data/lib/rubocop/cop/yoshiki/style/sorted_keyword_arguments.rb +140 -0
- data/lib/yoshiki/version.rb +1 -1
- data/lib/yoshiki.rb +6 -0
- data/yoshiki.gemspec +2 -2
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 94f5b98825fc1b7b536720e8e7b0357924b84084fccd31c46b0a61edab59c4ec
|
|
4
|
+
data.tar.gz: fc265b7932404fa8820b90e06a1b58c3b05c24a55de8bf83e95e068ba361a450
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2280c3f8309b8330f930b62b10484376b4258745046d030b08619587b174f06beae39c321f705a29c78e790476851cb58460fe4a0d3d3370dae9f3a4155dc7dc
|
|
7
|
+
data.tar.gz: 95caa06f31683f9c1676d1802cfc6eb66e29fb6bdea94b5d67e722399fcc68c914f047a25984071b236b44767bc5a6d6ef68ae9a0db8ff21de04353449eb9048
|
data/.rubocop-defaults-rails.yml
CHANGED
|
@@ -105,6 +105,8 @@ Rails/SquishedSQLHeredocs: # new in 2.8
|
|
|
105
105
|
Enabled: true
|
|
106
106
|
Rails/StripHeredoc: # new in 2.15
|
|
107
107
|
Enabled: true
|
|
108
|
+
Rails/StrongParametersExpect: # new in 2.29
|
|
109
|
+
Enabled: true # requires rails 8.0+
|
|
108
110
|
Rails/TimeZoneAssignment: # new in 2.10
|
|
109
111
|
Enabled: true
|
|
110
112
|
Rails/ThreeStateBooleanColumn: # new in 2.19
|
data/.rubocop-defaults.yml
CHANGED
|
@@ -271,8 +271,6 @@ Style/InPatternThen: # new in 1.16
|
|
|
271
271
|
Enabled: true
|
|
272
272
|
Style/ItAssignment: # new in 1.70
|
|
273
273
|
Enabled: true
|
|
274
|
-
Style/ItBlockParameter: # new in 1.75
|
|
275
|
-
Enabled: true
|
|
276
274
|
Style/KeywordArgumentsMerging: # new in 1.68
|
|
277
275
|
Enabled: true
|
|
278
276
|
Style/MagicCommentFormat: # new in 1.35
|
data/.yoshiki-haml.yml
ADDED
data/.yoshiki-rails.yml
CHANGED
|
@@ -40,8 +40,6 @@ Rails/ShortI18n: # new in 2.7
|
|
|
40
40
|
EnforcedStyle: aggressive # default is `conservative`
|
|
41
41
|
Rails/SkipsModelValidations:
|
|
42
42
|
Enabled: false
|
|
43
|
-
Rails/StrongParametersExpect: # new in 2.29
|
|
44
|
-
Enabled: false # requires rails 8.0+
|
|
45
43
|
Rails/ToFormattedS: # new in 2.15
|
|
46
44
|
Enabled: false # prefer to_s(:format)
|
|
47
45
|
Rails/ToSWithArgument: # new in 2.16
|
|
@@ -63,11 +61,15 @@ Style/Documentation: # this isn't a rails cop, but this config only makes sense
|
|
|
63
61
|
- app/jobs/application_job.rb
|
|
64
62
|
- app/mailers/application_mailer.rb
|
|
65
63
|
- app/models/application_record.rb
|
|
64
|
+
- app/models/current.rb
|
|
65
|
+
- app/models/session.rb
|
|
66
|
+
- app/models/user.rb
|
|
66
67
|
- config/application.rb
|
|
67
68
|
- db/migrate/**/*
|
|
68
69
|
- lib/application_responder.rb
|
|
69
70
|
- spec/**/*
|
|
70
71
|
- test/**/*
|
|
72
|
+
|
|
71
73
|
Style/EmptyMethod:
|
|
72
74
|
Exclude:
|
|
73
75
|
- app/controllers/**/*_controller.rb
|
data/.yoshiki-ruby.yml
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- yoshiki
|
|
3
|
+
|
|
1
4
|
AllCops:
|
|
2
5
|
DisplayCopNames: true
|
|
3
6
|
DisplayStyleGuide: true
|
|
@@ -131,6 +134,8 @@ Style/DoubleNegation:
|
|
|
131
134
|
Style/FrozenStringLiteralComment:
|
|
132
135
|
EnforcedStyle: never # huzzah for ruby 3.4
|
|
133
136
|
SafeAutoCorrect: true # default is `false`
|
|
137
|
+
Style/ItBlockParameter: # new in 1.75
|
|
138
|
+
Enabled: false # Yoshiki/Style/ItBlockParameter (force_single_line)
|
|
134
139
|
Style/MethodDefParentheses:
|
|
135
140
|
EnforcedStyle: require_no_parentheses
|
|
136
141
|
Style/MixinUsage:
|
|
@@ -177,3 +182,18 @@ Style/SymbolArray:
|
|
|
177
182
|
MinSize: 3 # default is `2`
|
|
178
183
|
Style/TrailingUnderscoreVariable:
|
|
179
184
|
AllowNamedUnderscoreVariables: false # defaults to `true`
|
|
185
|
+
|
|
186
|
+
Yoshiki/Layout/HangingKeywordArguments:
|
|
187
|
+
Enabled: true
|
|
188
|
+
MinKeywords: 2
|
|
189
|
+
Yoshiki/Lint/ParenthesizeValueOmission:
|
|
190
|
+
Enabled: true
|
|
191
|
+
MinKeywords: 1
|
|
192
|
+
Yoshiki/Style/ItBlockParameter:
|
|
193
|
+
Enabled: true
|
|
194
|
+
EnforcedStyle: force_single_line
|
|
195
|
+
SupportedStyles:
|
|
196
|
+
- force_single_line
|
|
197
|
+
Yoshiki/Style/SortedKeywordArguments:
|
|
198
|
+
Enabled: true
|
|
199
|
+
MinKeywords: 2
|
data/Gemfile
CHANGED
|
@@ -9,8 +9,10 @@ gem 'rake'
|
|
|
9
9
|
gem 'rspec'
|
|
10
10
|
|
|
11
11
|
# For developing optional configs (not runtime deps of the gem)
|
|
12
|
+
gem 'haml'
|
|
12
13
|
gem 'rubocop-capybara', '3.0.0'
|
|
13
14
|
gem 'rubocop-factory_bot', '2.28.0'
|
|
15
|
+
gem 'rubocop-haml', '0.3.1'
|
|
14
16
|
gem 'rubocop-minitest', '0.40.0'
|
|
15
17
|
gem 'rubocop-rails', '2.36.0'
|
|
16
18
|
gem 'rubocop-rspec', '3.10.2'
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
yoshiki (11.0.0.pre.
|
|
4
|
+
yoshiki (11.0.0.pre.8)
|
|
5
5
|
rubocop (= 1.89.0)
|
|
6
6
|
rubocop-performance (= 1.26.1)
|
|
7
7
|
rubocop-rake (= 0.7.1)
|
|
@@ -31,6 +31,13 @@ GEM
|
|
|
31
31
|
date (3.5.1)
|
|
32
32
|
diff-lcs (1.6.2)
|
|
33
33
|
drb (2.2.3)
|
|
34
|
+
haml (7.4.1)
|
|
35
|
+
prism (>= 1.1.0)
|
|
36
|
+
temple (>= 0.8.2)
|
|
37
|
+
thor
|
|
38
|
+
tilt
|
|
39
|
+
hamli (0.6.0)
|
|
40
|
+
temple
|
|
34
41
|
i18n (1.15.2)
|
|
35
42
|
concurrent-ruby (~> 1.0)
|
|
36
43
|
io-console (0.8.2)
|
|
@@ -94,6 +101,10 @@ GEM
|
|
|
94
101
|
rubocop-factory_bot (2.28.0)
|
|
95
102
|
lint_roller (~> 1.1)
|
|
96
103
|
rubocop (~> 1.72, >= 1.72.1)
|
|
104
|
+
rubocop-haml (0.3.1)
|
|
105
|
+
hamli (~> 0.5)
|
|
106
|
+
lint_roller (>= 1.1)
|
|
107
|
+
rubocop (>= 1.72.1)
|
|
97
108
|
rubocop-minitest (0.40.0)
|
|
98
109
|
lint_roller (~> 1.1)
|
|
99
110
|
rubocop (>= 1.75.0, < 2.0)
|
|
@@ -122,6 +133,9 @@ GEM
|
|
|
122
133
|
ruby-progressbar (1.13.0)
|
|
123
134
|
securerandom (0.4.1)
|
|
124
135
|
stringio (3.2.0)
|
|
136
|
+
temple (0.10.7)
|
|
137
|
+
thor (1.5.0)
|
|
138
|
+
tilt (2.9.0)
|
|
125
139
|
tzinfo (2.0.6)
|
|
126
140
|
concurrent-ruby (~> 1.0)
|
|
127
141
|
unicode-display_width (3.2.0)
|
|
@@ -134,12 +148,14 @@ PLATFORMS
|
|
|
134
148
|
arm64-darwin-25
|
|
135
149
|
|
|
136
150
|
DEPENDENCIES
|
|
151
|
+
haml
|
|
137
152
|
pry
|
|
138
153
|
psych
|
|
139
154
|
rake
|
|
140
155
|
rspec
|
|
141
156
|
rubocop-capybara (= 3.0.0)
|
|
142
157
|
rubocop-factory_bot (= 2.28.0)
|
|
158
|
+
rubocop-haml (= 0.3.1)
|
|
143
159
|
rubocop-minitest (= 0.40.0)
|
|
144
160
|
rubocop-rails (= 2.36.0)
|
|
145
161
|
rubocop-rspec (= 3.10.2)
|
data/README.md
CHANGED
|
@@ -56,6 +56,19 @@ inherit_gem:
|
|
|
56
56
|
- .yoshiki-minitest.yml
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
Haml — add the plugin, then inherit:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
gem 'rubocop-haml'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
inherit_gem:
|
|
67
|
+
yoshiki:
|
|
68
|
+
- .yoshiki-ruby.yml # or .yoshiki-rails.yml
|
|
69
|
+
- .yoshiki-haml.yml
|
|
70
|
+
```
|
|
71
|
+
|
|
59
72
|
Capybara — add the plugin, then inherit:
|
|
60
73
|
|
|
61
74
|
```ruby
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
require 'haml'
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Yoshiki
|
|
6
|
+
module Haml
|
|
7
|
+
|
|
8
|
+
# Walk a Haml AST and rewrite sibling blank lines.
|
|
9
|
+
class SiblingBlankSource
|
|
10
|
+
|
|
11
|
+
MSG_MISSING = 'Add a blank line between sibling layout elements.'.freeze
|
|
12
|
+
MSG_EXTRA = 'Remove the blank line between packed sibling leaves.'.freeze
|
|
13
|
+
CONTROL = %w[else elsif when rescue ensure].freeze
|
|
14
|
+
|
|
15
|
+
def rewrite source
|
|
16
|
+
apply(source.lines, events_for(operations(source))).join
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def operations source
|
|
20
|
+
ast = parse(source)
|
|
21
|
+
return [] unless ast
|
|
22
|
+
|
|
23
|
+
ops = []
|
|
24
|
+
collect(ast, source.lines, ops)
|
|
25
|
+
ops
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def parse source
|
|
31
|
+
::Haml::Parser.new({}).call(source)
|
|
32
|
+
rescue ::Haml::Error
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def collect node, lines, ops
|
|
37
|
+
node.children.each_cons(2) { |left, right| pair(left, right, lines, ops) }
|
|
38
|
+
node.children.each { collect(it, lines, ops) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pair left, right, lines, ops
|
|
42
|
+
return unless left.line && right.line
|
|
43
|
+
|
|
44
|
+
expected = expected_blanks(left, right)
|
|
45
|
+
blanks = blank_lines(left, right, lines)
|
|
46
|
+
return if blanks.size == expected
|
|
47
|
+
|
|
48
|
+
ops << offense(right.line, expected, blanks)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def offense line, expected, blanks
|
|
52
|
+
if blanks.size < expected
|
|
53
|
+
{ kind: :missing, line:, message: MSG_MISSING }
|
|
54
|
+
else
|
|
55
|
+
{ kind: :extra, line:, message: MSG_EXTRA, remove: blanks.drop(expected) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def expected_blanks left, right
|
|
60
|
+
return 0 if packed?(left) || packed?(right)
|
|
61
|
+
return 0 if leaf?(left) && leaf?(right)
|
|
62
|
+
|
|
63
|
+
1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def packed? node
|
|
67
|
+
node.type == :doctype ||
|
|
68
|
+
(node.type == :silent_script && CONTROL.include?(node.value[:keyword].to_s))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def leaf? node
|
|
72
|
+
node.type != :filter && node.children.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def blank_lines left, right, lines
|
|
76
|
+
first = finish(left) + 1
|
|
77
|
+
last = right.line - 1
|
|
78
|
+
return [] if last < first
|
|
79
|
+
|
|
80
|
+
(first..last).select { lines[it - 1].to_s.strip.empty? }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def finish node
|
|
84
|
+
extra = node.type == :filter ? node.value[:text].to_s.lines.size : 0
|
|
85
|
+
last = (node.line || 0) + extra
|
|
86
|
+
node.children.reduce(last) { |memo, child| [ memo, finish(child) ].max }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def events_for ops
|
|
90
|
+
ops.flat_map { events_for_op(it) }
|
|
91
|
+
.sort_by { |lineno, kind| [ lineno, kind == :delete ? 0 : 1 ] }
|
|
92
|
+
.reverse
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def events_for_op operation
|
|
96
|
+
return [ [ operation[:line], :insert ] ] if operation[:kind] == :missing
|
|
97
|
+
|
|
98
|
+
operation[:remove].map { [ it, :delete ] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def apply lines, events
|
|
102
|
+
events.each do |lineno, kind|
|
|
103
|
+
if kind == :delete
|
|
104
|
+
lines.delete_at(lineno - 1)
|
|
105
|
+
else
|
|
106
|
+
lines.insert(lineno - 1, "\n")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
lines
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Blank line between layout siblings; packed leaf units stay packed.
|
|
115
|
+
class SiblingBlankLines < Base
|
|
116
|
+
|
|
117
|
+
include RangeHelp
|
|
118
|
+
|
|
119
|
+
MSG_MISSING = SiblingBlankSource::MSG_MISSING
|
|
120
|
+
MSG_EXTRA = SiblingBlankSource::MSG_EXTRA
|
|
121
|
+
|
|
122
|
+
def on_new_investigation
|
|
123
|
+
inspect_haml
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def on_other_file
|
|
127
|
+
inspect_haml
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def rewrite_source source
|
|
131
|
+
SiblingBlankSource.new.rewrite(source)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def inspect_haml
|
|
137
|
+
original = @current_original
|
|
138
|
+
return unless original&.path&.end_with?('.haml')
|
|
139
|
+
return if original.instance_variable_get(:@_haml_sibling_blank_lines)
|
|
140
|
+
|
|
141
|
+
original.instance_variable_set(:@_haml_sibling_blank_lines, true)
|
|
142
|
+
ops = SiblingBlankSource.new.operations(original.raw_source)
|
|
143
|
+
ops.each { add_offense(line_range(original, it[:line]), message: it[:message]) }
|
|
144
|
+
write_corrected(original, ops)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def write_corrected original, ops
|
|
148
|
+
return if ops.empty? || !@options[:autocorrect] || @options[:stdin]
|
|
149
|
+
|
|
150
|
+
path = original.file_path
|
|
151
|
+
return unless path && File.exist?(path)
|
|
152
|
+
|
|
153
|
+
File.write(path, SiblingBlankSource.new.rewrite(original.raw_source))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def line_range original, lineno
|
|
157
|
+
source_range(original.buffer, lineno, 0)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module Yoshiki
|
|
4
|
+
# Shared helpers for Yoshiki cops that inspect keyword-only method calls.
|
|
5
|
+
module KeywordOnlyArguments
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def keyword_only_call? node, min_keywords:
|
|
10
|
+
return false unless node.arguments.one?
|
|
11
|
+
|
|
12
|
+
arg = node.arguments.first
|
|
13
|
+
return false unless arg.hash_type? && !arg.braces?
|
|
14
|
+
return false if arg.children.any? { !it.pair_type? }
|
|
15
|
+
|
|
16
|
+
arg.pairs.size >= min_keywords
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def keyword_hash node
|
|
20
|
+
node.arguments.first
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def value_omission? node
|
|
24
|
+
keyword_hash(node).pairs.any?(&:value_omission?)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def first_arg_on_method_line? node
|
|
28
|
+
first_pair = keyword_hash(node).pairs.first
|
|
29
|
+
same_line?(node.loc.selector, first_pair)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def braced_block? node
|
|
33
|
+
node.block_node&.braces?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def comments_inside? node
|
|
37
|
+
range = node.source_range
|
|
38
|
+
processed_source.comments.any? { range.contains?(it.loc.expression) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def syntax_like_method_call? node
|
|
42
|
+
node.implicit_call? || node.operator_method?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def disallowed_call? node
|
|
46
|
+
allowed_method?(node.method_name) ||
|
|
47
|
+
braced_block?(node) ||
|
|
48
|
+
comments_inside?(node) ||
|
|
49
|
+
syntax_like_method_call?(node)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def pair_key_name pair
|
|
53
|
+
key = pair.key
|
|
54
|
+
return key.value.to_s if key.sym_type?
|
|
55
|
+
return key.source if key.respond_to?(:source)
|
|
56
|
+
|
|
57
|
+
key.inspect
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def hanging_indent node
|
|
61
|
+
# Align under the first keyword: one char after method name (` ` or `(`).
|
|
62
|
+
' ' * (node.loc.selector.column + node.method_name.size + 1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_pairs pairs, indent
|
|
66
|
+
lines = pairs.map { it.source.strip }
|
|
67
|
+
first, *rest = lines
|
|
68
|
+
return first if rest.empty?
|
|
69
|
+
|
|
70
|
+
([ first ] + rest.map { "#{ indent }#{ it }" }).join(",\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module Yoshiki
|
|
4
|
+
module Layout
|
|
5
|
+
# Prefers multiline keyword-only method calls in hanging form: first
|
|
6
|
+
# keyword on the same line as the method, continuations aligned under
|
|
7
|
+
# the first keyword.
|
|
8
|
+
#
|
|
9
|
+
# Ordinary kwargs omit parentheses. Hash value omission (`key:`) keeps
|
|
10
|
+
# parentheses (see Yoshiki/Lint/ParenthesizeValueOmission); this cop only
|
|
11
|
+
# hangs inside existing parens for those calls.
|
|
12
|
+
#
|
|
13
|
+
# Key order is Yoshiki/Style/SortedKeywordArguments. Padding is
|
|
14
|
+
# Layout/HashAlignment.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# annotations(
|
|
19
|
+
# read_only_hint: true,
|
|
20
|
+
# destructive_hint: false
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# # good
|
|
24
|
+
# annotations read_only_hint: true,
|
|
25
|
+
# destructive_hint: false
|
|
26
|
+
#
|
|
27
|
+
# @example Hash value omission (parentheses required; hang inside them)
|
|
28
|
+
# # bad
|
|
29
|
+
# annotations(
|
|
30
|
+
# destructive_hint:,
|
|
31
|
+
# read_only_hint:
|
|
32
|
+
# )
|
|
33
|
+
#
|
|
34
|
+
# # good
|
|
35
|
+
# annotations(destructive_hint:,
|
|
36
|
+
# read_only_hint:)
|
|
37
|
+
class HangingKeywordArguments < Base
|
|
38
|
+
|
|
39
|
+
include AllowedMethods
|
|
40
|
+
include KeywordOnlyArguments
|
|
41
|
+
include ReparsedEquivalence
|
|
42
|
+
extend AutoCorrector
|
|
43
|
+
|
|
44
|
+
MSG_OMIT_PARENS = [
|
|
45
|
+
'Use hanging keyword arguments ',
|
|
46
|
+
'(first kwarg on the method line, no parentheses).'
|
|
47
|
+
].join.freeze
|
|
48
|
+
MSG_KEEP_PARENS = [
|
|
49
|
+
'Use hanging keyword arguments with parentheses ',
|
|
50
|
+
'when using hash value omission ',
|
|
51
|
+
'(first kwarg on the method line).'
|
|
52
|
+
].join.freeze
|
|
53
|
+
|
|
54
|
+
def on_send node
|
|
55
|
+
return unless eligible?(node)
|
|
56
|
+
|
|
57
|
+
(@pending ||= []) << node
|
|
58
|
+
end
|
|
59
|
+
alias on_csend on_send
|
|
60
|
+
|
|
61
|
+
def on_investigation_end
|
|
62
|
+
verified_by_reparse(@pending || []).each do |node|
|
|
63
|
+
add_offense(offense_range(node), message: message_for(node)) do |corrector|
|
|
64
|
+
autocorrect(corrector, node)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
@pending = []
|
|
68
|
+
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def apply_reparse_correction corrector, node
|
|
75
|
+
autocorrect(corrector, node)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def eligible? node
|
|
79
|
+
return false unless keyword_only_call?(node, min_keywords: min_keywords)
|
|
80
|
+
return false unless multiline_keywords?(node)
|
|
81
|
+
return false if first_arg_on_method_line?(node)
|
|
82
|
+
return false if disallowed_call?(node)
|
|
83
|
+
|
|
84
|
+
# Value omission must stay parenthesized; only restyle when parens exist.
|
|
85
|
+
return false if value_omission?(node) && !node.parenthesized?
|
|
86
|
+
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def multiline_keywords? node
|
|
91
|
+
node.multiline? || keyword_hash(node).multiline?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def min_keywords
|
|
95
|
+
cop_config.fetch('MinKeywords', 2)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def message_for node
|
|
99
|
+
value_omission?(node) ? MSG_KEEP_PARENS : MSG_OMIT_PARENS
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def offense_range node
|
|
103
|
+
if node.parenthesized?
|
|
104
|
+
node.loc.begin.join(node.loc.end)
|
|
105
|
+
else
|
|
106
|
+
keyword_hash(node).source_range
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def autocorrect corrector, node
|
|
111
|
+
rendered = render_pairs(keyword_hash(node).pairs.to_a, hanging_indent(node))
|
|
112
|
+
if value_omission?(node)
|
|
113
|
+
replace_keep_parens(corrector, node, rendered)
|
|
114
|
+
elsif node.parenthesized?
|
|
115
|
+
replace_omit_parens(corrector, node, rendered)
|
|
116
|
+
else
|
|
117
|
+
corrector.replace(keyword_hash(node).source_range, rendered)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def replace_omit_parens corrector, node, rendered
|
|
122
|
+
range = node.loc.selector.end.join(node.loc.end)
|
|
123
|
+
corrector.replace(range, " #{ rendered }")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def replace_keep_parens corrector, node, rendered
|
|
127
|
+
range = node.loc.begin.join(node.loc.end)
|
|
128
|
+
corrector.replace(range, "(#{ rendered })")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module Yoshiki
|
|
4
|
+
module Lint
|
|
5
|
+
# Requires parentheses around method calls that use hash value omission
|
|
6
|
+
# (`key:`). Bare forms can absorb a following expression as the last
|
|
7
|
+
# keyword's value (silent semantic change).
|
|
8
|
+
#
|
|
9
|
+
# Layout (hanging first kwarg) is Yoshiki/Layout/HangingKeywordArguments.
|
|
10
|
+
# Key order is Yoshiki/Style/SortedKeywordArguments.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# annotations destructive_hint:,
|
|
15
|
+
# read_only_hint:
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# annotations(destructive_hint:,
|
|
19
|
+
# read_only_hint:)
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# # bad
|
|
23
|
+
# annotations destructive_hint:, read_only_hint:
|
|
24
|
+
#
|
|
25
|
+
# # good
|
|
26
|
+
# annotations(destructive_hint:, read_only_hint:)
|
|
27
|
+
class ParenthesizeValueOmission < Base
|
|
28
|
+
|
|
29
|
+
include AllowedMethods
|
|
30
|
+
include KeywordOnlyArguments
|
|
31
|
+
include ReparsedEquivalence
|
|
32
|
+
extend AutoCorrector
|
|
33
|
+
|
|
34
|
+
MSG = [
|
|
35
|
+
'Parenthesize method calls that use hash value omission ',
|
|
36
|
+
'(`key:`) so a following expression is not absorbed as a value.'
|
|
37
|
+
].join.freeze
|
|
38
|
+
|
|
39
|
+
def on_send node
|
|
40
|
+
return unless eligible?(node)
|
|
41
|
+
|
|
42
|
+
(@pending ||= []) << node
|
|
43
|
+
end
|
|
44
|
+
alias on_csend on_send
|
|
45
|
+
|
|
46
|
+
def on_investigation_end
|
|
47
|
+
verified_by_reparse(@pending || []).each do |node|
|
|
48
|
+
add_offense(offense_range(node), message: MSG) do |corrector|
|
|
49
|
+
autocorrect(corrector, node)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
@pending = []
|
|
53
|
+
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def apply_reparse_correction corrector, node
|
|
60
|
+
autocorrect(corrector, node)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def eligible? node
|
|
64
|
+
return false if node.parenthesized?
|
|
65
|
+
return false unless keyword_only_call?(node, min_keywords: min_keywords)
|
|
66
|
+
return false unless value_omission?(node)
|
|
67
|
+
return false if disallowed_call?(node)
|
|
68
|
+
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def min_keywords
|
|
73
|
+
cop_config.fetch('MinKeywords', 1)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def offense_range node
|
|
77
|
+
keyword_hash(node).source_range
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def autocorrect corrector, node
|
|
81
|
+
hash = keyword_hash(node)
|
|
82
|
+
range = node.loc.selector.end.join(hash.source_range)
|
|
83
|
+
corrector.replace(range, "(#{ hash.source })")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module Yoshiki
|
|
4
|
+
module Style
|
|
5
|
+
# Checks for blocks with one argument where `it` block parameter can be used.
|
|
6
|
+
#
|
|
7
|
+
# `force_single_line` requires a single-line one-parameter block to use `it`.
|
|
8
|
+
# Multi-line blocks may keep a named parameter.
|
|
9
|
+
#
|
|
10
|
+
# @example EnforcedStyle: force_single_line
|
|
11
|
+
# # bad
|
|
12
|
+
# block { |named_param| do_something(named_param) }
|
|
13
|
+
# block { do_something(_1) }
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# block { do_something(it) }
|
|
17
|
+
# block do |named_param|
|
|
18
|
+
# do_something(named_param)
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
class ItBlockParameter < Base
|
|
22
|
+
|
|
23
|
+
include ConfigurableEnforcedStyle
|
|
24
|
+
extend TargetRubyVersion
|
|
25
|
+
extend AutoCorrector
|
|
26
|
+
|
|
27
|
+
MSG_USE_IT_PARAMETER = 'Use `it` block parameter.'.freeze
|
|
28
|
+
|
|
29
|
+
minimum_target_ruby_version 3.4
|
|
30
|
+
|
|
31
|
+
def on_block node
|
|
32
|
+
return unless force_single_line?
|
|
33
|
+
return unless node.single_line?
|
|
34
|
+
return unless node.arguments.one?
|
|
35
|
+
return unless node.first_argument.arg_type?
|
|
36
|
+
|
|
37
|
+
each_block_variable(node, node.first_argument.source) do |variable|
|
|
38
|
+
add_offense(variable, message: MSG_USE_IT_PARAMETER) do |corrector|
|
|
39
|
+
remove_block_arguments(corrector, node)
|
|
40
|
+
corrector.replace(variable, 'it')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def on_numblock node
|
|
46
|
+
return unless force_single_line?
|
|
47
|
+
return unless node.single_line?
|
|
48
|
+
return unless node.children[1] == 1
|
|
49
|
+
|
|
50
|
+
each_block_variable(node, '_1') do |variable|
|
|
51
|
+
add_offense(variable, message: MSG_USE_IT_PARAMETER) do |corrector|
|
|
52
|
+
corrector.replace(variable, 'it')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def force_single_line?
|
|
60
|
+
style == :force_single_line
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def remove_block_arguments corrector, node
|
|
64
|
+
range = node.arguments.source_range
|
|
65
|
+
range = range.adjust(end_pos: 1) if range.source_buffer.source[range.end_pos] == ' '
|
|
66
|
+
corrector.remove(range)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def each_block_variable node, name, &block
|
|
70
|
+
find_block_variables(node, name).each(&block)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def find_block_variables node, name
|
|
74
|
+
return [] unless node.body
|
|
75
|
+
|
|
76
|
+
vars = node.body.each_descendant(:lvar).select { it.source == name }
|
|
77
|
+
vars.unshift(node.body) if node.body.lvar_type? && node.body.source == name
|
|
78
|
+
vars
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module Yoshiki
|
|
4
|
+
module Style
|
|
5
|
+
# Enforces alphabetical order for keyword-only method call arguments.
|
|
6
|
+
#
|
|
7
|
+
# Layout is Yoshiki/Layout/HangingKeywordArguments. Value-omission
|
|
8
|
+
# parentheses are Yoshiki/Lint/ParenthesizeValueOmission. Padding is
|
|
9
|
+
# Layout/HashAlignment.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# annotations read_only_hint: true,
|
|
14
|
+
# destructive_hint: false
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# annotations destructive_hint: false,
|
|
18
|
+
# read_only_hint: true
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# # bad
|
|
22
|
+
# annotations(read_only_hint:, destructive_hint:)
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# annotations(destructive_hint:, read_only_hint:)
|
|
26
|
+
class SortedKeywordArguments < Base
|
|
27
|
+
|
|
28
|
+
include AllowedMethods
|
|
29
|
+
include KeywordOnlyArguments
|
|
30
|
+
include ReparsedEquivalence
|
|
31
|
+
extend AutoCorrector
|
|
32
|
+
|
|
33
|
+
MSG = 'Sort keyword arguments alphabetically by key name.'.freeze
|
|
34
|
+
|
|
35
|
+
def on_send node
|
|
36
|
+
return unless eligible?(node)
|
|
37
|
+
|
|
38
|
+
(@pending ||= []) << node
|
|
39
|
+
end
|
|
40
|
+
alias on_csend on_send
|
|
41
|
+
|
|
42
|
+
def on_investigation_end
|
|
43
|
+
verified_by_reparse(@pending || []).each do |node|
|
|
44
|
+
add_offense(offense_range(node), message: MSG) do |corrector|
|
|
45
|
+
autocorrect(corrector, node)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
@pending = []
|
|
49
|
+
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def apply_reparse_correction corrector, node
|
|
56
|
+
autocorrect(corrector, node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def eligible? node
|
|
60
|
+
return false unless keyword_only_call?(node, min_keywords: min_keywords)
|
|
61
|
+
return false if disallowed_call?(node)
|
|
62
|
+
return false if sorted?(node)
|
|
63
|
+
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def min_keywords
|
|
68
|
+
cop_config.fetch('MinKeywords', 2)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def sorted? node
|
|
72
|
+
names = keyword_hash(node).pairs.map { pair_key_name(it) }
|
|
73
|
+
names == names.sort
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def offense_range node
|
|
77
|
+
keyword_hash(node).source_range
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def autocorrect corrector, node
|
|
81
|
+
hash = keyword_hash(node)
|
|
82
|
+
sorted = hash.pairs.to_a.sort_by { pair_key_name(it) }
|
|
83
|
+
corrector.replace(hash.source_range, replacement_for(node, hash, sorted))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def replacement_for node, hash, pairs
|
|
87
|
+
return single_line_replacement(pairs) unless multiline_call?(node, hash)
|
|
88
|
+
|
|
89
|
+
multiline_replacement(node, hash, pairs)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def single_line_replacement pairs
|
|
93
|
+
pairs.map { it.source.strip }.join(', ')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def multiline_replacement node, hash, pairs
|
|
97
|
+
return render_pairs(pairs, hanging_indent(node)) if first_arg_on_method_line?(node)
|
|
98
|
+
|
|
99
|
+
# First arg on its own line inside parens: keep each pair on its own
|
|
100
|
+
# line with the original first-pair indentation.
|
|
101
|
+
indent = ' ' * hash.pairs.first.loc.expression.column
|
|
102
|
+
pairs.map { it.source.strip }.join(",\n#{ indent }")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def multiline_call? node, hash
|
|
106
|
+
node.multiline? || hash.multiline?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Pair order is the only intentional AST change; normalize both sides.
|
|
110
|
+
def normalize_reparsed_ast node
|
|
111
|
+
return node unless node.is_a?(::Parser::AST::Node)
|
|
112
|
+
|
|
113
|
+
children = node.children.map { normalize_reparsed_ast(it) }
|
|
114
|
+
return sorted_hash_node(node, children) if node.type == :hash
|
|
115
|
+
|
|
116
|
+
node.updated(nil, children)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def sorted_hash_node node, children
|
|
120
|
+
pairs, others = children.partition { pair_node?(it) }
|
|
121
|
+
pairs = pairs.sort_by { pair_sort_key(it) }
|
|
122
|
+
node.updated(nil, pairs + others)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def pair_node? node
|
|
126
|
+
node.is_a?(::Parser::AST::Node) && node.type == :pair
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def pair_sort_key pair
|
|
130
|
+
key = pair.children.first
|
|
131
|
+
return key.children.first.to_s if key.is_a?(::Parser::AST::Node) && key.type == :sym
|
|
132
|
+
|
|
133
|
+
key.inspect
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/yoshiki/version.rb
CHANGED
data/lib/yoshiki.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
require 'yoshiki/version'
|
|
2
|
+
require 'rubocop'
|
|
3
|
+
require 'rubocop/cop/yoshiki/keyword_only_arguments'
|
|
4
|
+
require 'rubocop/cop/yoshiki/layout/hanging_keyword_arguments'
|
|
5
|
+
require 'rubocop/cop/yoshiki/lint/parenthesize_value_omission'
|
|
6
|
+
require 'rubocop/cop/yoshiki/style/it_block_parameter'
|
|
7
|
+
require 'rubocop/cop/yoshiki/style/sorted_keyword_arguments'
|
|
2
8
|
|
|
3
9
|
# Dev Fu! Style
|
|
4
10
|
module Yoshiki
|
data/yoshiki.gemspec
CHANGED
|
@@ -11,9 +11,9 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.description = 'Dev Fu! Style for rubocop.'
|
|
12
12
|
spec.homepage = 'https://gitlab.com/devfu/yoshiki'
|
|
13
13
|
spec.license = 'MIT'
|
|
14
|
-
spec.files = `git ls-files -z`.split("\x0").reject {
|
|
14
|
+
spec.files = `git ls-files -z`.split("\x0").reject { it.match %r{^(test|spec|features)/} }
|
|
15
15
|
spec.bindir = 'exe'
|
|
16
|
-
spec.executables = spec.files.grep(%r{^exe/}) {
|
|
16
|
+
spec.executables = spec.files.grep(%r{^exe/}) { File.basename it }
|
|
17
17
|
spec.require_paths = %w[ lib ]
|
|
18
18
|
|
|
19
19
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
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.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BM5k
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- ".rubocop-defaults.yml"
|
|
70
70
|
- ".rubocop.yml"
|
|
71
71
|
- ".yoshiki-capybara.yml"
|
|
72
|
+
- ".yoshiki-haml.yml"
|
|
72
73
|
- ".yoshiki-minitest.yml"
|
|
73
74
|
- ".yoshiki-rails-rspec.yml"
|
|
74
75
|
- ".yoshiki-rails.yml"
|
|
@@ -82,6 +83,12 @@ files:
|
|
|
82
83
|
- README.md
|
|
83
84
|
- Rakefile
|
|
84
85
|
- bin/console
|
|
86
|
+
- lib/rubocop/cop/yoshiki/haml/sibling_blank_lines.rb
|
|
87
|
+
- lib/rubocop/cop/yoshiki/keyword_only_arguments.rb
|
|
88
|
+
- lib/rubocop/cop/yoshiki/layout/hanging_keyword_arguments.rb
|
|
89
|
+
- lib/rubocop/cop/yoshiki/lint/parenthesize_value_omission.rb
|
|
90
|
+
- lib/rubocop/cop/yoshiki/style/it_block_parameter.rb
|
|
91
|
+
- lib/rubocop/cop/yoshiki/style/sorted_keyword_arguments.rb
|
|
85
92
|
- lib/yoshiki.rb
|
|
86
93
|
- lib/yoshiki/examples.rb
|
|
87
94
|
- lib/yoshiki/version.rb
|