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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d220afa1f162c7d97efcab1ceeaac3c137a6c0577c82457df40d06266b8a7fca
4
- data.tar.gz: 0f5246cd97229c9c360333ef540e33858460396250cae3f058aec766fbd2f074
3
+ metadata.gz: 94f5b98825fc1b7b536720e8e7b0357924b84084fccd31c46b0a61edab59c4ec
4
+ data.tar.gz: fc265b7932404fa8820b90e06a1b58c3b05c24a55de8bf83e95e068ba361a450
5
5
  SHA512:
6
- metadata.gz: 697454ec8255d170136feeff721b6699d64f3bb2d8014aeb5968018a3f25aa7b1fb3e2bd45044bcde5dd8d096e61f24858cc1faa1e768e5fa6589afa42be334e
7
- data.tar.gz: f455dcd95cc45f3b8f1468427082725b058203a228d8e6ed372e9edad888c49078e65003fc50295637c9ddaaf9080013f8ecfc26b3ce83569b6a850b0b96b6aa
6
+ metadata.gz: 2280c3f8309b8330f930b62b10484376b4258745046d030b08619587b174f06beae39c321f705a29c78e790476851cb58460fe4a0d3d3370dae9f3a4155dc7dc
7
+ data.tar.gz: 95caa06f31683f9c1676d1802cfc6eb66e29fb6bdea94b5d67e722399fcc68c914f047a25984071b236b44767bc5a6d6ef68ae9a0db8ff21de04353449eb9048
@@ -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
@@ -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
@@ -0,0 +1,10 @@
1
+ plugins:
2
+ - rubocop-haml
3
+
4
+ require:
5
+ - rubocop/cop/yoshiki/haml/sibling_blank_lines
6
+
7
+ Yoshiki/Haml/SiblingBlankLines:
8
+ Enabled: true
9
+ Include:
10
+ - '**/*.haml'
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.7)
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
@@ -1,5 +1,5 @@
1
1
  module Yoshiki
2
2
 
3
- VERSION = '11.0.0-7'.freeze
3
+ VERSION = '11.0.0-8'.freeze
4
4
 
5
5
  end
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 { |f| f.match %r{^(test|spec|features)/} }
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/}) { |f| File.basename f }
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.7
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