shirobai 2026.0620.0600 → 2026.0620.2000

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: a0cc3b10f1ce2bb8d770cc56e43ed3deccea614daf0c487d0eb7cf37783715c1
4
- data.tar.gz: f8bd98cccca3c2a8e9505b0f6b74af94b19dbab1e4079f9c8c39c2db93b193c0
3
+ metadata.gz: 2da350f27fcd3ff47681028d0508eade78a0ced26a5b96007985da947ab8a292
4
+ data.tar.gz: 680562385119492ea485da4929b883bba60ff15129f6fd9a2e70d230245aaff4
5
5
  SHA512:
6
- metadata.gz: 00ab1d29d7c85d1f155784a5138c5b7f5828360e4df62268b8106e166f4cce43491d09e8ac49e30de5b6e8f63dbec2cd664382e433dd5e5b29424e5734db6ce6
7
- data.tar.gz: b5d947fc4f44f9bc600966f12fa98218dcb2f3527b6711f677a1ca44a0e870593597931fb6db204ffd98cf2ed02b10b48ac0a3ce758e47ed4d742bb1fe7e0169
6
+ metadata.gz: 06e8d2e281b447c0ca65762f86616575b8e4b2a9af55f135b28027479a5ddc3b95d3cf6e6e74ce0970863d4f16776e66b77a6c901e6168e3828f8eae6b5b9ade
7
+ data.tar.gz: a8b07df2f070dabb4c12c9f91bc88dc31483b668559e51df92d3a146deec8b2e85687d2d547f557c46f9796d9cc995512c6f6a992cdff79349bb6c7407fe5b97
@@ -174,12 +174,14 @@ impl<'a> Visitor<'a> {
174
174
  String::from_utf8_lossy(&slice[..e]).into_owned()
175
175
  }
176
176
 
177
- /// `match = /\S.*/.match(do_loc.source_line)`: the do/`{` line's content
178
- /// from its first non-space char, and that column. Returns `(source, line,
179
- /// column)`.
180
- fn do_source_line(&self, do_start: usize) -> (String, usize, usize) {
181
- let line = self.line_of(do_start);
182
- let ls = self.line_index.line_start(do_start);
177
+ /// `match = /\S.*/.match(anchor_loc.source_line)`: returns `(source, line,
178
+ /// column)` from the anchor line's first non-space char. The anchor is
179
+ /// normally the `do`/`{` line, but shifts to the method dispatch line
180
+ /// when the `do` line is a continuation of multiline arguments.
181
+ fn do_source_line(&self, do_start: usize, anchor_start: Option<usize>) -> (String, usize, usize) {
182
+ let ref_offset = anchor_start.unwrap_or(do_start);
183
+ let line = self.line_of(ref_offset);
184
+ let ls = self.line_index.line_start(ref_offset);
183
185
  let starts = self.line_index.line_starts();
184
186
  let le = if line < starts.len() {
185
187
  let mut e = starts[line] - 1;
@@ -193,12 +195,26 @@ impl<'a> Visitor<'a> {
193
195
  let first_non_space = (ls..le)
194
196
  .find(|&i| !is_space_byte(self.source[i]))
195
197
  .unwrap_or(ls);
196
- // column == number of chars from line start to first non-space.
197
198
  let column = self.column(first_non_space);
198
- // `match[0]` is `\S.*` — from the first non-space to end of line.
199
199
  let text = String::from_utf8_lossy(&self.source[first_non_space..le]).into_owned();
200
200
  (text, line, column)
201
201
  }
202
+
203
+ /// The indentation of the original `do`/`{` line (not the anchor).
204
+ fn do_line_indentation(&self, do_start: usize) -> usize {
205
+ let ls = self.line_index.line_start(do_start);
206
+ let starts = self.line_index.line_starts();
207
+ let line = self.line_of(do_start);
208
+ let le = if line < starts.len() {
209
+ starts[line] - 1
210
+ } else {
211
+ self.source.len()
212
+ };
213
+ let first_non_space = (ls..le)
214
+ .find(|&i| !is_space_byte(self.source[i]))
215
+ .unwrap_or(ls);
216
+ self.column(first_non_space)
217
+ }
202
218
  }
203
219
 
204
220
  fn is_space_byte(b: u8) -> bool {
@@ -226,6 +242,9 @@ struct BlockLoc {
226
242
  open_start: usize,
227
243
  close_start: usize,
228
244
  close_end: usize,
245
+ /// When the `do`/`{` line starts inside a multiline method argument,
246
+ /// anchor on the method dispatch line instead of the do line.
247
+ anchor_start: Option<usize>,
229
248
  }
230
249
 
231
250
  impl<'a> Visitor<'a> {
@@ -233,20 +252,37 @@ impl<'a> Visitor<'a> {
233
252
  // call / super / forwarding-super with a literal BlockNode.
234
253
  if let Some(call) = node.as_call_node() {
235
254
  if let Some(bn) = call.block().and_then(|b| b.as_block_node()) {
236
- return self.block_node_loc(&bn);
255
+ let mut arg_ranges: Vec<(usize, usize)> = Vec::new();
256
+ if let Some(args) = call.arguments() {
257
+ for arg in args.arguments().iter() {
258
+ arg_ranges.push(loc(&arg.location()));
259
+ }
260
+ }
261
+ let selector = call
262
+ .message_loc()
263
+ .map(|l| l.start_offset())
264
+ .unwrap_or(call.location().start_offset());
265
+ return self.block_node_loc_with_anchor(&bn, &arg_ranges, selector);
237
266
  }
238
267
  return None;
239
268
  }
240
269
  if let Some(sup) = node.as_super_node() {
241
270
  if let Some(bn) = sup.block().and_then(|b| b.as_block_node()) {
242
- return self.block_node_loc(&bn);
271
+ let mut arg_ranges: Vec<(usize, usize)> = Vec::new();
272
+ if let Some(args) = sup.arguments() {
273
+ for arg in args.arguments().iter() {
274
+ arg_ranges.push(loc(&arg.location()));
275
+ }
276
+ }
277
+ let selector = sup.keyword_loc().start_offset();
278
+ return self.block_node_loc_with_anchor(&bn, &arg_ranges, selector);
243
279
  }
244
280
  return None;
245
281
  }
246
282
  if let Some(fsup) = node.as_forwarding_super_node() {
247
- // `ForwardingSuperNode::block()` returns the `BlockNode` directly.
248
283
  if let Some(bn) = fsup.block() {
249
- return self.block_node_loc(&bn);
284
+ let selector = node.location().start_offset();
285
+ return self.block_node_loc_with_anchor(&bn, &[], selector);
250
286
  }
251
287
  return None;
252
288
  }
@@ -254,24 +290,72 @@ impl<'a> Visitor<'a> {
254
290
  if let Some(lam) = node.as_lambda_node() {
255
291
  let (open_start, _) = loc(&lam.opening_loc());
256
292
  let (close_start, close_end) = loc(&lam.closing_loc());
293
+ // Lambda parameters are the "arguments" for the anchor check.
294
+ let mut arg_ranges: Vec<(usize, usize)> = Vec::new();
295
+ if let Some(params) = lam.parameters() {
296
+ let ploc = params.location();
297
+ arg_ranges.push(loc(&ploc));
298
+ }
299
+ let selector = lam.operator_loc().start_offset();
300
+ let anchor_start = self.do_line_anchor(open_start, &arg_ranges, &[], selector);
257
301
  return Some(BlockLoc {
258
302
  open_start,
259
303
  close_start,
260
304
  close_end,
305
+ anchor_start,
261
306
  });
262
307
  }
263
308
  None
264
309
  }
265
310
 
266
- fn block_node_loc(&self, bn: &ruby_prism::BlockNode<'_>) -> Option<BlockLoc> {
311
+ fn block_node_loc_with_anchor(
312
+ &self,
313
+ bn: &ruby_prism::BlockNode<'_>,
314
+ send_arg_ranges: &[(usize, usize)],
315
+ selector_start: usize,
316
+ ) -> Option<BlockLoc> {
267
317
  let (open_start, _) = loc(&bn.opening_loc());
268
318
  let (close_start, close_end) = loc(&bn.closing_loc());
319
+ let mut block_param_ranges: Vec<(usize, usize)> = Vec::new();
320
+ if let Some(params) = bn.parameters() {
321
+ let ploc = params.location();
322
+ block_param_ranges.push(loc(&ploc));
323
+ }
324
+ let anchor_start = self.do_line_anchor(open_start, send_arg_ranges, &block_param_ranges, selector_start);
269
325
  Some(BlockLoc {
270
326
  open_start,
271
327
  close_start,
272
328
  close_end,
329
+ anchor_start,
273
330
  })
274
331
  }
332
+
333
+ /// When the `do`/`{` line begins inside one of the call's arguments or the
334
+ /// block's parameters, return the method selector start offset as the
335
+ /// alignment anchor.
336
+ fn do_line_anchor(
337
+ &self,
338
+ open_start: usize,
339
+ send_arg_ranges: &[(usize, usize)],
340
+ block_param_ranges: &[(usize, usize)],
341
+ selector_start: usize,
342
+ ) -> Option<usize> {
343
+ let ls = self.line_index.line_start(open_start);
344
+ let first_char_pos = (ls..open_start)
345
+ .find(|&i| !is_space_byte(self.source[i]))
346
+ .unwrap_or(open_start);
347
+
348
+ let inside = send_arg_ranges
349
+ .iter()
350
+ .chain(block_param_ranges.iter())
351
+ .any(|&(s, e)| s <= first_char_pos && first_char_pos < e);
352
+
353
+ if inside {
354
+ Some(selector_start)
355
+ } else {
356
+ None
357
+ }
358
+ }
275
359
  }
276
360
 
277
361
  // --- Lineage / alignment-target selection. ---
@@ -429,6 +513,11 @@ impl<'a> Visitor<'a> {
429
513
  fn on_block(&mut self, block_range: (usize, usize), bl: &BlockLoc) {
430
514
  let block_first_line = self.first_line_of(block_range.0);
431
515
  let start = self.start_for_block_node(block_range, block_first_line);
516
+ let start = if self.style == STYLE_START_OF_LINE {
517
+ self.start_for_line_node(start)
518
+ } else {
519
+ start
520
+ };
432
521
  self.check_block_alignment(start, block_range, bl);
433
522
  }
434
523
 
@@ -447,9 +536,15 @@ impl<'a> Visitor<'a> {
447
536
  if start_col == end_col && self.style != STYLE_START_OF_BLOCK {
448
537
  return;
449
538
  }
450
- // compute_do_source_line_column.
451
- let (do_text, do_line, do_col) = self.do_source_line(bl.open_start);
452
- if end_col == do_col && self.style != STYLE_START_OF_LINE {
539
+ // compute_do_source_line_column (using anchor if available).
540
+ let (do_text, do_line, do_col) = self.do_source_line(bl.open_start, bl.anchor_start);
541
+ // permitted_do_line_columns: under `either`, both the anchor line's
542
+ // indentation and the original do line's indentation are accepted.
543
+ let mut permitted = vec![do_col];
544
+ if self.style == STYLE_EITHER && bl.anchor_start.is_some() {
545
+ permitted.push(self.do_line_indentation(bl.open_start));
546
+ }
547
+ if permitted.contains(&end_col) && self.style != STYLE_START_OF_LINE {
453
548
  return;
454
549
  }
455
550
  self.register_offense(start, block_range, bl, (do_text, do_line, do_col));
@@ -24,7 +24,7 @@
24
24
  //! `move_comment_before_block` and the `begin`/`end` wrapping of multiline
25
25
  //! rescue/ensure bodies.
26
26
 
27
- use ruby_prism::{CallNode, Node, StatementsNode, Visit};
27
+ use ruby_prism::{CallNode, Node, StatementsNode};
28
28
 
29
29
  type Range = (usize, usize);
30
30
 
@@ -328,7 +328,7 @@ impl<'a> Visitor<'a> {
328
328
  opening: Range,
329
329
  closing: Range,
330
330
  method_name: &str,
331
- node: &Node<'_>,
331
+ _node: &Node<'_>,
332
332
  body: Option<&Node<'_>>,
333
333
  send_arguments: bool,
334
334
  send_parenthesized: bool,
@@ -336,7 +336,7 @@ impl<'a> Visitor<'a> {
336
336
  let braces = self.source[opening.0] == b'{';
337
337
  // `BlockNode#multiline?` compares the delimiter lines.
338
338
  let multiline = self.source[opening.0..closing.0].contains(&b'\n');
339
- if self.proper_block_style(braces, multiline, block_range, method_name, node) {
339
+ if self.proper_block_style(braces, multiline, block_range, method_name, body) {
340
340
  return;
341
341
  }
342
342
  let message = self.message(braces, multiline, block_range, method_name);
@@ -366,9 +366,9 @@ impl<'a> Visitor<'a> {
366
366
  multiline: bool,
367
367
  block_range: Range,
368
368
  method_name: &str,
369
- node: &Node<'_>,
369
+ body: Option<&Node<'_>>,
370
370
  ) -> bool {
371
- if self.require_do_end(braces, multiline, node) {
371
+ if self.require_do_end(braces, multiline, body) {
372
372
  return true;
373
373
  }
374
374
  if self.list_in(method_name, &self.cfg.allowed_methods) {
@@ -391,15 +391,42 @@ impl<'a> Visitor<'a> {
391
391
  }
392
392
  }
393
393
 
394
- /// `require_do_end?`: a single-line `do`..`end` whose first descendant
395
- /// `resbody` carries an exception class list cannot use braces.
396
- fn require_do_end(&self, braces: bool, multiline: bool, node: &Node<'_>) -> bool {
394
+ /// `require_do_end?`: a single-line `do`..`end` block cannot use braces
395
+ /// when it contains `ensure` or a block-level `rescue` (as opposed to a
396
+ /// bare modifier rescue `expr rescue expr`).
397
+ fn require_do_end(&self, braces: bool, multiline: bool, body: Option<&Node<'_>>) -> bool {
397
398
  if braces || multiline {
398
399
  return false;
399
400
  }
400
- let mut finder = ResbodyFinder { found: None };
401
- finder.visit_children_of(node);
402
- finder.found.unwrap_or(false)
401
+ let Some(body) = body else { return false };
402
+ // Prism wraps block-level rescue/ensure in a BeginNode (implicit
403
+ // begin). Modifier rescue stays as a RescueModifierNode inside a
404
+ // StatementsNode.
405
+ if let Some(begin) = body.as_begin_node() {
406
+ if begin.ensure_clause().is_some() {
407
+ return true;
408
+ }
409
+ if let Some(rescue) = begin.rescue_clause() {
410
+ let has_protected_body = begin.statements().is_some();
411
+ return !is_modifier_rescue(&rescue, has_protected_body);
412
+ }
413
+ }
414
+ // Walk into StatementsNode: the body of a block is often a
415
+ // StatementsNode wrapping the actual content.
416
+ if let Some(stmts) = body.as_statements_node() {
417
+ for stmt in stmts.body().iter() {
418
+ if let Some(begin) = stmt.as_begin_node() {
419
+ if begin.ensure_clause().is_some() {
420
+ return true;
421
+ }
422
+ if let Some(rescue) = begin.rescue_clause() {
423
+ let has_protected_body = begin.statements().is_some();
424
+ return !is_modifier_rescue(&rescue, has_protected_body);
425
+ }
426
+ }
427
+ }
428
+ }
429
+ false
403
430
  }
404
431
 
405
432
  fn semantic_block_style(
@@ -1178,44 +1205,39 @@ fn parser_body_range(begin: &ruby_prism::BeginNode<'_>) -> Range {
1178
1205
  (start, end)
1179
1206
  }
1180
1207
 
1181
- /// `each_descendant(:resbody).first`, then `children.first&.array_type?`:
1182
- /// finds the first rescue clause (or rescue modifier) in parser preorder and
1183
- /// reports whether it carries an exception class list.
1184
- struct ResbodyFinder {
1185
- found: Option<bool>,
1186
- }
1187
-
1188
- impl ResbodyFinder {
1189
- /// Visit the children of the block wrapper without treating the wrapper
1190
- /// itself as a result.
1191
- fn visit_children_of(&mut self, node: &Node<'_>) {
1192
- if let Some(call) = node.as_call_node() {
1193
- ruby_prism::visit_call_node(self, &call);
1194
- } else if let Some(sup) = node.as_super_node() {
1195
- ruby_prism::visit_super_node(self, &sup);
1196
- } else if let Some(fsup) = node.as_forwarding_super_node() {
1197
- ruby_prism::visit_forwarding_super_node(self, &fsup);
1198
- } else if let Some(lambda) = node.as_lambda_node() {
1199
- ruby_prism::visit_lambda_node(self, &lambda);
1200
- }
1208
+ /// A bare modifier rescue: `expr rescue expr` — single branch, no
1209
+ /// exceptions list, no exception variable, no else. This form CAN be
1210
+ /// written with braces, unlike a block-level rescue.
1211
+ ///
1212
+ /// In Prism, block-level rescue creates a `BeginNode` containing a
1213
+ /// `RescueNode` chain. The `RescueNode` here is the first (and possibly
1214
+ /// only) rescue clause. Stock checks on the parser-gem `:rescue` wrapper:
1215
+ /// - `body.nil?` → no protected expression before the rescue keyword
1216
+ /// - `else_branch` has an else
1217
+ /// - `resbody_branches.one?` exactly one rescue clause
1218
+ /// - `resbody.exceptions.empty?` no exception class list
1219
+ /// - `resbody.exception_variable.nil?` no `=> e`
1220
+ ///
1221
+ /// A block-level `rescue` (even bare) always has the protected body at the
1222
+ /// `BeginNode` level (in `begin.statements()`), and the `RescueNode` itself
1223
+ /// has no `statements()` (only `exceptions`, `exception`, and `subsequent`).
1224
+ /// So a modifier rescue in this context is one with no exceptions, no
1225
+ /// exception variable, and no subsequent clause.
1226
+ fn is_modifier_rescue(rescue: &ruby_prism::RescueNode<'_>, has_protected_body: bool) -> bool {
1227
+ if !has_protected_body {
1228
+ return false;
1201
1229
  }
1202
- }
1203
-
1204
- impl<'pr> Visit<'pr> for ResbodyFinder {
1205
- fn visit_rescue_node(&mut self, node: &ruby_prism::RescueNode<'pr>) {
1206
- if self.found.is_none() {
1207
- self.found = Some(node.exceptions().iter().next().is_some());
1208
- }
1230
+ if rescue.subsequent().is_some() {
1231
+ return false;
1209
1232
  }
1210
-
1211
- fn visit_rescue_modifier_node(&mut self, node: &ruby_prism::RescueModifierNode<'pr>) {
1212
- // Parser order: the rescued expression subtree precedes the
1213
- // modifier's own (class-less) resbody.
1214
- self.visit(&node.expression());
1215
- if self.found.is_none() {
1216
- self.found = Some(false);
1217
- }
1233
+ let exceptions: Vec<_> = rescue.exceptions().iter().collect();
1234
+ if !exceptions.is_empty() {
1235
+ return false;
1236
+ }
1237
+ if rescue.reference().is_some() {
1238
+ return false;
1218
1239
  }
1240
+ true
1219
1241
  }
1220
1242
 
1221
1243
  impl super::dispatch::Rule for Visitor<'_> {
@@ -11,14 +11,11 @@
11
11
  //! That's the `Tip::Top(arg)` constructor-style call which looks like a
12
12
  //! constant reference at the call site (`Top(...)` would be `Integer(x)` /
13
13
  //! `String(x)`).
14
- //! - Skip when `java_type_node?(node)` — stock pattern
15
- //! `(send (const nil? :Java) _)`: the receiver is a top-level
16
- //! `ConstantReadNode` named `Java`. The pattern's `nil?` matches a nil
17
- //! receiver on the `const`, NOT a `cbase` receiver (`::Java::int` is FLAGGED
18
- //! by stock; verified by probe). So in prism the gate is
19
- //! `receiver.as_constant_read_node()` whose `name() == "Java"`. A
20
- //! `ConstantPathNode` (`::Java`) is a different prism node and structurally
21
- //! excluded.
14
+ //! - Skip when `java_interop?(node)` — walks the receiver chain to its root
15
+ //! and checks `java_root?` (= `(const nil? :Java)`). This excludes not just
16
+ //! `Java::int` but the entire chain `Java::com::something_method`.
17
+ //! The `cbase` form `::Java::int` is still FLAGGED because the root is a
18
+ //! `ConstantPathNode`, not a bare `ConstantReadNode`.
22
19
  //!
23
20
  //! Offense range = `node.loc.dot` = the prism `call_operator_loc()` range
24
21
  //! (`::`, two bytes). Autocorrect replaces those two bytes with `.`.
@@ -103,15 +100,11 @@ impl ColonMethodCallVisitor {
103
100
  return;
104
101
  }
105
102
 
106
- // `java_type_node?(node)` — stock pattern `(send (const nil? :Java) _)`.
107
- // The receiver is a top-level `ConstantReadNode` whose name is `Java`.
108
- // `ConstantPathNode` (the `cbase` form `::Java`) is a different node
109
- // kind and is correctly NOT excluded — `::Java::int` is flagged by
110
- // stock.
111
- let receiver = call.receiver().expect("receiver presence checked above");
112
- if let Some(const_read) = receiver.as_constant_read_node()
113
- && const_read.name().as_slice() == b"Java"
114
- {
103
+ // `java_interop?(node)` — walk the receiver chain to its root and
104
+ // check `java_root?` (= `(const nil? :Java)`). This excludes the
105
+ // entire `Java::com::something_method` chain, not just the first
106
+ // `Java::com` call.
107
+ if Self::java_interop(call) {
115
108
  return;
116
109
  }
117
110
 
@@ -120,6 +113,23 @@ impl ColonMethodCallVisitor {
120
113
  dot_end: op_end,
121
114
  });
122
115
  }
116
+
117
+ fn java_interop(call: &ruby_prism::CallNode<'_>) -> bool {
118
+ let mut node = call.receiver().expect("receiver presence checked by caller");
119
+ loop {
120
+ if let Some(const_read) = node.as_constant_read_node() {
121
+ return const_read.name().as_slice() == b"Java";
122
+ }
123
+ if let Some(inner_call) = node.as_call_node() {
124
+ match inner_call.receiver() {
125
+ Some(r) => node = r,
126
+ None => return false,
127
+ }
128
+ } else {
129
+ return false;
130
+ }
131
+ }
132
+ }
123
133
  }
124
134
 
125
135
  impl<'pr> Visit<'pr> for ColonMethodCallVisitor {
@@ -235,12 +245,9 @@ mod tests {
235
245
  }
236
246
 
237
247
  #[test]
238
- fn flags_after_java_then_user_call() {
239
- // `Java::foo::bar` — inner `Java::foo` is java-typed (excluded), but
240
- // outer `bar` (receiver = `Java::foo` CallNode) is flagged. `::` at
241
- // 9..11.
242
- let off = detect("Java::foo::bar\n");
243
- assert_eq!(off, vec![(9, 11)]);
248
+ fn accepts_java_interop_chain() {
249
+ // `Java::foo::bar` — root receiver is `Java`, entire chain excluded.
250
+ assert!(detect("Java::foo::bar\n").is_empty());
244
251
  }
245
252
 
246
253
  #[test]
@@ -85,6 +85,22 @@ impl<'a> Visitor<'a> {
85
85
  node.as_call_node().is_some_and(|c| c.is_safe_navigation())
86
86
  }
87
87
 
88
+ fn is_csend_or_parenthesized_csend(node: &Node<'_>) -> bool {
89
+ if Self::is_csend(node) {
90
+ return true;
91
+ }
92
+ if let Some(paren) = node.as_parentheses_node()
93
+ && let Some(body) = paren.body()
94
+ && let Some(stmts) = body.as_statements_node()
95
+ {
96
+ let children: Vec<_> = stmts.body().iter().collect();
97
+ if children.len() == 1 {
98
+ return Self::is_csend(&children[0]);
99
+ }
100
+ }
101
+ false
102
+ }
103
+
88
104
  fn is_comparison(call: &CallNode<'_>) -> bool {
89
105
  COMPARISON_OPERATORS.contains(&call.name().as_slice())
90
106
  }
@@ -220,7 +236,7 @@ impl<'a> Visitor<'a> {
220
236
  let Some(receiver) = call.receiver() else {
221
237
  return;
222
238
  };
223
- if !Self::is_csend(&receiver) {
239
+ if !Self::is_csend_or_parenthesized_csend(&receiver) {
224
240
  return;
225
241
  }
226
242
  let method = call.name().as_slice();
@@ -14,8 +14,12 @@
14
14
  //! path zero-cost on the Ruby side. See `lib/shirobai/cop/lint/self_assignment.rb`.
15
15
 
16
16
  use ruby_prism::{
17
- CallNode, ClassVariableWriteNode, ConstantPathNode, ConstantPathWriteNode, ConstantWriteNode,
18
- GlobalVariableWriteNode, InstanceVariableWriteNode, LocalVariableAndWriteNode,
17
+ CallAndWriteNode, CallNode, CallOrWriteNode, ClassVariableAndWriteNode,
18
+ ClassVariableOrWriteNode, ClassVariableWriteNode, ConstantAndWriteNode,
19
+ ConstantOrWriteNode, ConstantPathNode, ConstantPathWriteNode, ConstantWriteNode,
20
+ GlobalVariableAndWriteNode, GlobalVariableOrWriteNode, GlobalVariableWriteNode,
21
+ IndexAndWriteNode, IndexOrWriteNode, InstanceVariableAndWriteNode,
22
+ InstanceVariableOrWriteNode, InstanceVariableWriteNode, LocalVariableAndWriteNode,
19
23
  LocalVariableOrWriteNode, LocalVariableWriteNode, MultiWriteNode, Node, Visit,
20
24
  };
21
25
 
@@ -214,6 +218,177 @@ impl<'s> SelfAssignmentVisitor<'s> {
214
218
  self.push(loc.start_offset(), loc.end_offset(), anchor);
215
219
  }
216
220
 
221
+ fn check_ivar_or_asgn(&mut self, node: &InstanceVariableOrWriteNode<'_>) {
222
+ let value = node.value();
223
+ let Some(rhs) = value.as_instance_variable_read_node() else { return };
224
+ if rhs.name().as_slice() != node.name().as_slice() {
225
+ return;
226
+ }
227
+ let loc = node.location();
228
+ let anchor = node.name_loc().end_offset();
229
+ self.push(loc.start_offset(), loc.end_offset(), anchor);
230
+ }
231
+
232
+ fn check_ivar_and_asgn(&mut self, node: &InstanceVariableAndWriteNode<'_>) {
233
+ let value = node.value();
234
+ let Some(rhs) = value.as_instance_variable_read_node() else { return };
235
+ if rhs.name().as_slice() != node.name().as_slice() {
236
+ return;
237
+ }
238
+ let loc = node.location();
239
+ let anchor = node.name_loc().end_offset();
240
+ self.push(loc.start_offset(), loc.end_offset(), anchor);
241
+ }
242
+
243
+ fn check_cvar_or_asgn(&mut self, node: &ClassVariableOrWriteNode<'_>) {
244
+ let value = node.value();
245
+ let Some(rhs) = value.as_class_variable_read_node() else { return };
246
+ if rhs.name().as_slice() != node.name().as_slice() {
247
+ return;
248
+ }
249
+ let loc = node.location();
250
+ let anchor = node.name_loc().end_offset();
251
+ self.push(loc.start_offset(), loc.end_offset(), anchor);
252
+ }
253
+
254
+ fn check_cvar_and_asgn(&mut self, node: &ClassVariableAndWriteNode<'_>) {
255
+ let value = node.value();
256
+ let Some(rhs) = value.as_class_variable_read_node() else { return };
257
+ if rhs.name().as_slice() != node.name().as_slice() {
258
+ return;
259
+ }
260
+ let loc = node.location();
261
+ let anchor = node.name_loc().end_offset();
262
+ self.push(loc.start_offset(), loc.end_offset(), anchor);
263
+ }
264
+
265
+ fn check_gvar_or_asgn(&mut self, node: &GlobalVariableOrWriteNode<'_>) {
266
+ let value = node.value();
267
+ let Some(rhs) = value.as_global_variable_read_node() else { return };
268
+ if rhs.name().as_slice() != node.name().as_slice() {
269
+ return;
270
+ }
271
+ let loc = node.location();
272
+ let anchor = node.name_loc().end_offset();
273
+ self.push(loc.start_offset(), loc.end_offset(), anchor);
274
+ }
275
+
276
+ fn check_gvar_and_asgn(&mut self, node: &GlobalVariableAndWriteNode<'_>) {
277
+ let value = node.value();
278
+ let Some(rhs) = value.as_global_variable_read_node() else { return };
279
+ if rhs.name().as_slice() != node.name().as_slice() {
280
+ return;
281
+ }
282
+ let loc = node.location();
283
+ let anchor = node.name_loc().end_offset();
284
+ self.push(loc.start_offset(), loc.end_offset(), anchor);
285
+ }
286
+
287
+ fn check_const_or_and_asgn(&mut self, name: &[u8], value: &Node<'_>, loc_start: usize, loc_end: usize, anchor: usize) {
288
+ let (rhs_ns, rhs_short) = match resolve_const_rhs(value) {
289
+ Some(p) => p,
290
+ None => return,
291
+ };
292
+ if name != rhs_short {
293
+ return;
294
+ }
295
+ if !namespaces_equal(None, rhs_ns.as_ref()) {
296
+ return;
297
+ }
298
+ self.push(loc_start, loc_end, anchor);
299
+ }
300
+
301
+ fn check_const_or_asgn(&mut self, node: &ConstantOrWriteNode<'_>) {
302
+ let value = node.value();
303
+ let loc = node.location();
304
+ let anchor = node.name_loc().end_offset();
305
+ self.check_const_or_and_asgn(node.name().as_slice(), &value, loc.start_offset(), loc.end_offset(), anchor);
306
+ }
307
+
308
+ fn check_const_and_asgn(&mut self, node: &ConstantAndWriteNode<'_>) {
309
+ let value = node.value();
310
+ let loc = node.location();
311
+ let anchor = node.name_loc().end_offset();
312
+ self.check_const_or_and_asgn(node.name().as_slice(), &value, loc.start_offset(), loc.end_offset(), anchor);
313
+ }
314
+
315
+ fn check_call_or_and_asgn_common(&mut self, receiver: Option<Node<'_>>, read_name: &[u8], value: &Node<'_>, loc_start: usize, loc_end: usize) {
316
+ let Some(rhs_call) = value.as_call_node() else { return };
317
+ let rhs_args: Vec<Node<'_>> = rhs_call
318
+ .arguments()
319
+ .map(|a| a.arguments().iter().collect())
320
+ .unwrap_or_default();
321
+ if !rhs_args.is_empty() {
322
+ return;
323
+ }
324
+ if rhs_call.name().as_slice() != read_name {
325
+ return;
326
+ }
327
+ let anchor = receiver
328
+ .as_ref()
329
+ .map(|r| r.location().end_offset())
330
+ .unwrap_or(loc_start);
331
+ if !ast_equal(receiver, rhs_call.receiver(), self.source) {
332
+ return;
333
+ }
334
+ self.push(loc_start, loc_end, anchor);
335
+ }
336
+
337
+ fn check_call_or_asgn(&mut self, node: &CallOrWriteNode<'_>) {
338
+ let loc = node.location();
339
+ self.check_call_or_and_asgn_common(node.receiver(), node.read_name().as_slice(), &node.value(), loc.start_offset(), loc.end_offset());
340
+ }
341
+
342
+ fn check_call_and_asgn(&mut self, node: &CallAndWriteNode<'_>) {
343
+ let loc = node.location();
344
+ self.check_call_or_and_asgn_common(node.receiver(), node.read_name().as_slice(), &node.value(), loc.start_offset(), loc.end_offset());
345
+ }
346
+
347
+ fn check_index_or_and_asgn_common(&mut self, receiver: Option<Node<'_>>, arguments: Option<ruby_prism::ArgumentsNode<'_>>, value: &Node<'_>, loc_start: usize, loc_end: usize) {
348
+ let Some(rhs_call) = value.as_call_node() else { return };
349
+ if rhs_call.name().as_slice() != b"[]" {
350
+ return;
351
+ }
352
+ let anchor = receiver
353
+ .as_ref()
354
+ .map(|r| r.location().end_offset())
355
+ .unwrap_or(loc_start);
356
+ if !ast_equal(receiver, rhs_call.receiver(), self.source) {
357
+ return;
358
+ }
359
+ let lhs_args: Vec<Node<'_>> = arguments
360
+ .map(|a| a.arguments().iter().collect())
361
+ .unwrap_or_default();
362
+ if lhs_args.iter().any(|n| is_call_node(n)) {
363
+ return;
364
+ }
365
+ let rhs_args: Vec<Node<'_>> = rhs_call
366
+ .arguments()
367
+ .map(|a| a.arguments().iter().collect())
368
+ .unwrap_or_default();
369
+ if lhs_args.len() != rhs_args.len() {
370
+ return;
371
+ }
372
+ if !lhs_args
373
+ .iter()
374
+ .zip(&rhs_args)
375
+ .all(|(a, b)| ast_equal_node(a, b, self.source))
376
+ {
377
+ return;
378
+ }
379
+ self.push(loc_start, loc_end, anchor);
380
+ }
381
+
382
+ fn check_index_or_asgn(&mut self, node: &IndexOrWriteNode<'_>) {
383
+ let loc = node.location();
384
+ self.check_index_or_and_asgn_common(node.receiver(), node.arguments(), &node.value(), loc.start_offset(), loc.end_offset());
385
+ }
386
+
387
+ fn check_index_and_asgn(&mut self, node: &IndexAndWriteNode<'_>) {
388
+ let loc = node.location();
389
+ self.check_index_or_and_asgn_common(node.receiver(), node.arguments(), &node.value(), loc.start_offset(), loc.end_offset());
390
+ }
391
+
217
392
  fn check_send(&mut self, call: &CallNode<'_>) {
218
393
  // `on_send` covers `[]=` (key assignment) and `assignment_method?`
219
394
  // (attribute setters whose name ends with `=`).
@@ -484,6 +659,54 @@ impl<'pr, 's> Visit<'pr> for SelfAssignmentVisitor<'s> {
484
659
  self.check_and_asgn(node);
485
660
  ruby_prism::visit_local_variable_and_write_node(self, node);
486
661
  }
662
+ fn visit_instance_variable_or_write_node(&mut self, node: &InstanceVariableOrWriteNode<'pr>) {
663
+ self.check_ivar_or_asgn(node);
664
+ ruby_prism::visit_instance_variable_or_write_node(self, node);
665
+ }
666
+ fn visit_instance_variable_and_write_node(&mut self, node: &InstanceVariableAndWriteNode<'pr>) {
667
+ self.check_ivar_and_asgn(node);
668
+ ruby_prism::visit_instance_variable_and_write_node(self, node);
669
+ }
670
+ fn visit_class_variable_or_write_node(&mut self, node: &ClassVariableOrWriteNode<'pr>) {
671
+ self.check_cvar_or_asgn(node);
672
+ ruby_prism::visit_class_variable_or_write_node(self, node);
673
+ }
674
+ fn visit_class_variable_and_write_node(&mut self, node: &ClassVariableAndWriteNode<'pr>) {
675
+ self.check_cvar_and_asgn(node);
676
+ ruby_prism::visit_class_variable_and_write_node(self, node);
677
+ }
678
+ fn visit_global_variable_or_write_node(&mut self, node: &GlobalVariableOrWriteNode<'pr>) {
679
+ self.check_gvar_or_asgn(node);
680
+ ruby_prism::visit_global_variable_or_write_node(self, node);
681
+ }
682
+ fn visit_global_variable_and_write_node(&mut self, node: &GlobalVariableAndWriteNode<'pr>) {
683
+ self.check_gvar_and_asgn(node);
684
+ ruby_prism::visit_global_variable_and_write_node(self, node);
685
+ }
686
+ fn visit_constant_or_write_node(&mut self, node: &ConstantOrWriteNode<'pr>) {
687
+ self.check_const_or_asgn(node);
688
+ ruby_prism::visit_constant_or_write_node(self, node);
689
+ }
690
+ fn visit_constant_and_write_node(&mut self, node: &ConstantAndWriteNode<'pr>) {
691
+ self.check_const_and_asgn(node);
692
+ ruby_prism::visit_constant_and_write_node(self, node);
693
+ }
694
+ fn visit_call_or_write_node(&mut self, node: &CallOrWriteNode<'pr>) {
695
+ self.check_call_or_asgn(node);
696
+ ruby_prism::visit_call_or_write_node(self, node);
697
+ }
698
+ fn visit_call_and_write_node(&mut self, node: &CallAndWriteNode<'pr>) {
699
+ self.check_call_and_asgn(node);
700
+ ruby_prism::visit_call_and_write_node(self, node);
701
+ }
702
+ fn visit_index_or_write_node(&mut self, node: &IndexOrWriteNode<'pr>) {
703
+ self.check_index_or_asgn(node);
704
+ ruby_prism::visit_index_or_write_node(self, node);
705
+ }
706
+ fn visit_index_and_write_node(&mut self, node: &IndexAndWriteNode<'pr>) {
707
+ self.check_index_and_asgn(node);
708
+ ruby_prism::visit_index_and_write_node(self, node);
709
+ }
487
710
  fn visit_call_node(&mut self, node: &CallNode<'pr>) {
488
711
  self.check_send(node);
489
712
  ruby_prism::visit_call_node(self, node);
@@ -510,6 +733,30 @@ impl<'s> super::dispatch::Rule for SelfAssignmentVisitor<'s> {
510
733
  self.check_or_asgn(&n);
511
734
  } else if let Some(n) = node.as_local_variable_and_write_node() {
512
735
  self.check_and_asgn(&n);
736
+ } else if let Some(n) = node.as_instance_variable_or_write_node() {
737
+ self.check_ivar_or_asgn(&n);
738
+ } else if let Some(n) = node.as_instance_variable_and_write_node() {
739
+ self.check_ivar_and_asgn(&n);
740
+ } else if let Some(n) = node.as_class_variable_or_write_node() {
741
+ self.check_cvar_or_asgn(&n);
742
+ } else if let Some(n) = node.as_class_variable_and_write_node() {
743
+ self.check_cvar_and_asgn(&n);
744
+ } else if let Some(n) = node.as_global_variable_or_write_node() {
745
+ self.check_gvar_or_asgn(&n);
746
+ } else if let Some(n) = node.as_global_variable_and_write_node() {
747
+ self.check_gvar_and_asgn(&n);
748
+ } else if let Some(n) = node.as_constant_or_write_node() {
749
+ self.check_const_or_asgn(&n);
750
+ } else if let Some(n) = node.as_constant_and_write_node() {
751
+ self.check_const_and_asgn(&n);
752
+ } else if let Some(n) = node.as_call_or_write_node() {
753
+ self.check_call_or_asgn(&n);
754
+ } else if let Some(n) = node.as_call_and_write_node() {
755
+ self.check_call_and_asgn(&n);
756
+ } else if let Some(n) = node.as_index_or_write_node() {
757
+ self.check_index_or_asgn(&n);
758
+ } else if let Some(n) = node.as_index_and_write_node() {
759
+ self.check_index_and_asgn(&n);
513
760
  } else if let Some(call) = node.as_call_node() {
514
761
  self.check_send(&call);
515
762
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shirobai
4
- VERSION = "2026.0620.0600"
4
+ VERSION = "2026.0620.2000"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shirobai
3
3
  version: !ruby/object:Gem::Version
4
- version: 2026.0620.0600
4
+ version: 2026.0620.2000
5
5
  platform: ruby
6
6
  authors:
7
7
  - fusagiko / takayamaki
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 1.87.0
18
+ version: 1.88.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 1.87.0
25
+ version: 1.88.0
26
26
  executables: []
27
27
  extensions:
28
28
  - ext/shirobai/Cargo.toml