shirobai 2026.0713.1900 → 2026.0820.0300

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: c30aa19df5a7127a9ef22225af01bb0cf954f22bf0d6b24b3a0a078f92c048a5
4
- data.tar.gz: 72b8d186516eee2a565482698b6bc44d981407f1c89d8cdc4eafe9d05b7903e8
3
+ metadata.gz: dba7704fcfe48ea2b368a6eae1afadb52dbfaaa926c4388f9d74a038f64c8bdf
4
+ data.tar.gz: 17e02721945c202d5cd39487dc155b66c42180f21e6692e58c3c0a8818b2d246
5
5
  SHA512:
6
- metadata.gz: 2ca531e38a5565b0b3e132baf5b5ced764ac8fe019df25fd1e5a910ef0ff871adbcdbe1fd9afd2bf76330066501548c486a69cf64482383fef9e87ed4dff67e2
7
- data.tar.gz: e5831ba58f7e9e93ab79ecefe6a57d96d02a334ee8653a8c356b9dc56f3c2a558022cb509d96f97c65d422f4e6053d74a14a0e1f010a6c95ee747c1acc51ff98
6
+ metadata.gz: de5ec9277179462e47e99b30b209e261a6bfc3468ef63f62e49bcf3d1e88ff92ff3fa5aa20d1c1dd67b52934a5197d2b99f334f944c9e5cf9da074d38271ad97
7
+ data.tar.gz: 8dd4eae8d0a5f3fa820fed008736b4c0381445eab74b6ee2f665958727281e658bba766c405883f6fa4e1fac9f9ba8d443df04310435ace19c5bb779ff2de374
data/Cargo.lock CHANGED
@@ -356,7 +356,7 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
356
356
 
357
357
  [[package]]
358
358
  name = "shirobai"
359
- version = "2026.713.1900"
359
+ version = "2026.820.300"
360
360
  dependencies = [
361
361
  "magnus",
362
362
  "shirobai-core",
@@ -364,7 +364,7 @@ dependencies = [
364
364
 
365
365
  [[package]]
366
366
  name = "shirobai-core"
367
- version = "2026.713.1900"
367
+ version = "2026.820.300"
368
368
  dependencies = [
369
369
  "libc",
370
370
  "ruby-prism",
data/Cargo.toml CHANGED
@@ -7,7 +7,7 @@ resolver = "3"
7
7
  # is crate 2026.709.0 -- cargo rejects leading zeros in numeric segments, and
8
8
  # Gem::Version normalizes to the same integers, so the two stay value-equal).
9
9
  [workspace.package]
10
- version = "2026.713.1900"
10
+ version = "2026.820.300"
11
11
 
12
12
  # Release tuning. The hot path crosses crate boundaries (ext cdylib -> core
13
13
  # check_* -> ruby-prism); without LTO the cross-crate calls are not inlined and
@@ -98,6 +98,16 @@ pub struct Config {
98
98
 
99
99
  #[derive(Debug, Clone)]
100
100
  pub struct AmbiguousBlockAssociationOffense {
101
+ /// 0 — the classic paren ambiguity (stock `on_send`): the fields read as
102
+ /// documented below and the offense is correctable.
103
+ ///
104
+ /// 1 — a `do...end` block binding to the outer method while an enumerable
105
+ /// call sits in the arguments (stock `on_block`, 1.89, rubocop#14835).
106
+ /// `[start, end)` is the INNER call's range (the offense highlight AND
107
+ /// the `AllowedPatterns` match target), `param_*` is the inner call's
108
+ /// method NAME range, `inner_send_*` is the OUTER call's method NAME
109
+ /// range, and the `ac_*` fields are zero (stock has no autocorrect).
110
+ pub kind: u8,
101
111
  /// Start byte of the OUTER call's source range (offense highlight).
102
112
  pub start_offset: usize,
103
113
  /// End byte of the OUTER call's source range.
@@ -299,6 +309,7 @@ impl<'s> AmbiguousBlockAssociationVisitor<'s> {
299
309
  inner_send_loc.expect("inner_send_loc set for the CallNode arm");
300
310
 
301
311
  self.offenses.push(AmbiguousBlockAssociationOffense {
312
+ kind: 0,
302
313
  start_offset: outer_loc.start_offset(),
303
314
  end_offset: outer_loc.end_offset(),
304
315
  param_start: last_loc.start_offset(),
@@ -310,6 +321,166 @@ impl<'s> AmbiguousBlockAssociationVisitor<'s> {
310
321
  ac_close_pos: last_loc.end_offset(),
311
322
  });
312
323
  }
324
+
325
+ /// Stock `on_block` (1.89, rubocop#14835): a `do...end` block that binds
326
+ /// to the outer method while the arguments end with an enumerable call
327
+ /// that was likely meant to receive it.
328
+ ///
329
+ /// ```ruby
330
+ /// def on_block(node)
331
+ /// return if node.braces?
332
+ /// send_node = node.send_node
333
+ /// block_method_arg = find_ambiguous_block_method(node)
334
+ /// return unless block_method_arg
335
+ /// add_offense(block_method_arg, message: format(MSG_DO_END_BLOCK, …))
336
+ /// end
337
+ /// ```
338
+ ///
339
+ /// `find_ambiguous_block_method` requires `send_node.send_type?` (a
340
+ /// csend outer is excluded), arguments, and no parentheses; the candidate
341
+ /// search (`find_block_method_arg`) walks the last argument's subtree for
342
+ /// a `:call` whose source ends where the outer send's source ends
343
+ /// (parser's outer send excludes the block, so that end is the last
344
+ /// argument's end), whose name is one of `BLOCK_METHODS`, and which has
345
+ /// no arguments (parser counts a block-pass as an argument). A prism
346
+ /// CallNode carrying a literal BlockNode is a parser `:block`, not a
347
+ /// `:call` — never a match, but its children are still searched.
348
+ fn check_do_end(&mut self, call: &ruby_prism::CallNode<'_>) {
349
+ let Some(block) = call.block() else { return };
350
+ let Some(block_node) = block.as_block_node() else {
351
+ return;
352
+ };
353
+ // `node.braces?` — only `do...end` blocks are ambiguous this way.
354
+ if block_node.opening_loc().as_slice() != b"do" {
355
+ return;
356
+ }
357
+ // `send_node.send_type?` — parser csend is a different type.
358
+ if call
359
+ .call_operator_loc()
360
+ .is_some_and(|l| l.as_slice() == b"&.")
361
+ {
362
+ return;
363
+ }
364
+ let Some(args_node) = call.arguments() else { return };
365
+ let arg_vec: Vec<_> = args_node.arguments().iter().collect();
366
+ let Some(last_arg) = arg_vec.last() else { return };
367
+ // `send_node.parenthesized?`
368
+ if call.opening_loc().is_some() {
369
+ return;
370
+ }
371
+ let target_end = last_arg.location().end_offset();
372
+ let mut finder = FindBlockMethodArg {
373
+ target_end,
374
+ result: None,
375
+ };
376
+ finder.visit(last_arg);
377
+ let Some((start, end, name_start, name_end)) = finder.result else {
378
+ return;
379
+ };
380
+ // `allowed_method?(block_method_arg.method_name)`
381
+ if self
382
+ .allowed_methods
383
+ .iter()
384
+ .any(|n| n.as_slice() == &self.source[name_start..name_end])
385
+ {
386
+ return;
387
+ }
388
+ // `matches_allowed_pattern?(block_method_arg.source)` — pre-applied
389
+ // by the wrapper, keyed by the inner call's source bytes.
390
+ if self
391
+ .allowed_inner_sources
392
+ .iter()
393
+ .any(|s| s.as_slice() == &self.source[start..end])
394
+ {
395
+ return;
396
+ }
397
+ let Some(outer_msg) = call.message_loc() else { return };
398
+ self.offenses.push(AmbiguousBlockAssociationOffense {
399
+ kind: 1,
400
+ start_offset: start,
401
+ end_offset: end,
402
+ param_start: name_start,
403
+ param_end: name_end,
404
+ inner_send_start: outer_msg.start_offset(),
405
+ inner_send_end: outer_msg.end_offset(),
406
+ ac_open_start: 0,
407
+ ac_open_end: 0,
408
+ ac_close_pos: 0,
409
+ });
410
+ }
411
+ }
412
+
413
+ /// Stock `BLOCK_METHODS`: the enumerable methods a stray `do` block was
414
+ /// probably meant for.
415
+ fn is_block_method(name: &[u8]) -> bool {
416
+ matches!(
417
+ name,
418
+ b"map"
419
+ | b"collect"
420
+ | b"flat_map"
421
+ | b"collect_concat"
422
+ | b"select"
423
+ | b"filter"
424
+ | b"find_all"
425
+ | b"reject"
426
+ | b"find"
427
+ | b"detect"
428
+ | b"each"
429
+ | b"each_with_object"
430
+ | b"each_with_index"
431
+ | b"reduce"
432
+ | b"inject"
433
+ | b"sort_by"
434
+ | b"min_by"
435
+ | b"max_by"
436
+ | b"group_by"
437
+ | b"filter_map"
438
+ )
439
+ }
440
+
441
+ /// `send_node.last_argument.each_node(:call).find { … }`: the first call in
442
+ /// the last argument's subtree whose source ends where the outer send's
443
+ /// source ends, named like an enumerable, and taking no arguments.
444
+ struct FindBlockMethodArg {
445
+ target_end: usize,
446
+ /// `(start, end, name_start, name_end)` of the found inner call.
447
+ result: Option<(usize, usize, usize, usize)>,
448
+ }
449
+
450
+ impl<'pr> Visit<'pr> for FindBlockMethodArg {
451
+ fn visit_call_node(&mut self, node: &ruby_prism::CallNode<'pr>) {
452
+ if self.result.is_some() {
453
+ return;
454
+ }
455
+ // A call with a literal block is a parser `:block`, not a `:call` —
456
+ // skip it as a match but keep searching its children.
457
+ let literal_block = node
458
+ .block()
459
+ .is_some_and(|b| b.as_block_node().is_some());
460
+ // Parser counts a block-pass (`&blk`) as an argument.
461
+ let has_args = node
462
+ .arguments()
463
+ .is_some_and(|a| a.arguments().iter().count() > 0)
464
+ || node
465
+ .block()
466
+ .is_some_and(|b| b.as_block_argument_node().is_some());
467
+ if !literal_block
468
+ && !has_args
469
+ && node.location().end_offset() == self.target_end
470
+ && is_block_method(node.name().as_slice())
471
+ && let Some(msg) = node.message_loc()
472
+ {
473
+ let loc = node.location();
474
+ self.result = Some((
475
+ loc.start_offset(),
476
+ loc.end_offset(),
477
+ msg.start_offset(),
478
+ msg.end_offset(),
479
+ ));
480
+ return;
481
+ }
482
+ ruby_prism::visit_call_node(self, node);
483
+ }
313
484
  }
314
485
 
315
486
  /// rubocop-ast `OPERATOR_METHODS`. Identical to the table in
@@ -350,6 +521,7 @@ fn is_operator_method(name: &[u8]) -> bool {
350
521
  impl<'pr, 's> Visit<'pr> for AmbiguousBlockAssociationVisitor<'s> {
351
522
  fn visit_call_node(&mut self, node: &ruby_prism::CallNode<'pr>) {
352
523
  self.check_call(node);
524
+ self.check_do_end(node);
353
525
  ruby_prism::visit_call_node(self, node);
354
526
  }
355
527
  }
@@ -361,10 +533,11 @@ impl<'s> super::dispatch::Rule for AmbiguousBlockAssociationVisitor<'s> {
361
533
  Interest::ENTER_CALL,
362
534
  )
363
535
  }
364
-
536
+
365
537
  fn enter(&mut self, node: &Node<'_>) {
366
538
  if let Some(call) = node.as_call_node() {
367
539
  self.check_call(&call);
540
+ self.check_do_end(&call);
368
541
  }
369
542
  }
370
543
 
@@ -409,6 +582,47 @@ mod tests {
409
582
  assert_eq!(off.len(), 1);
410
583
  }
411
584
 
585
+ #[test]
586
+ fn do_end_binding_to_outer_method() {
587
+ // 1.89 (rubocop#14835): `render json: data.map do ... end` — Ruby
588
+ // binds the `do` block to `render`, so `map` is called without a
589
+ // block. Offense on the inner call, no autocorrect.
590
+ let off = detect("render json: data.map do |x|\n x\nend\n");
591
+ assert_eq!(off.len(), 1);
592
+ let o = &off[0];
593
+ assert_eq!(o.kind, 1);
594
+ assert_eq!((o.start_offset, o.end_offset), (13, 21)); // `data.map`
595
+ assert_eq!((o.param_start, o.param_end), (18, 21)); // `map`
596
+ assert_eq!((o.inner_send_start, o.inner_send_end), (0, 6)); // `render`
597
+ assert_eq!(o.ac_open_end, 0); // not correctable
598
+
599
+ // positional argument and safe navigation on the inner call
600
+ assert_eq!(detect("foo bar.select do |x|\n x\nend\n").len(), 1);
601
+ assert_eq!(detect("foo bar&.each do |x|\n x\nend\n").len(), 1);
602
+ }
603
+
604
+ #[test]
605
+ fn do_end_binding_not_flagged_when_unambiguous() {
606
+ // braces bind to the inner call; parentheses fix the binding
607
+ assert!(detect("render json: data.map { |x| x }\n").is_empty());
608
+ assert!(detect("render(json: data.map) do |x|\n x\nend\n").is_empty());
609
+ // a call chained past the enumerable was never a block candidate
610
+ assert!(detect("foo bar.map.to_a do |x|\n x\nend\n").is_empty());
611
+ // an inner call that already has arguments or a block-pass
612
+ assert!(detect("foo bar.reduce(0) do |a, b|\n a\nend\n").is_empty());
613
+ assert!(detect("foo bar.map(&:to_s) do |x|\n x\nend\n").is_empty());
614
+ // a method outside the enumerable list
615
+ assert!(detect("foo bar.frobnicate do |x|\n x\nend\n").is_empty());
616
+ // safe navigation on the outer call is not `send_type?`
617
+ assert!(detect("foo&.bar baz.map do |x|\n x\nend\n").is_empty());
618
+ }
619
+
620
+ #[test]
621
+ fn do_end_binding_respects_allowed_methods() {
622
+ let off = detect_with_allowed("foo bar.map do |x|\n x\nend\n", &["map"]);
623
+ assert!(off.is_empty());
624
+ }
625
+
412
626
  #[test]
413
627
  fn flags_rspec_change_default() {
414
628
  let off = detect("expect { order.expire }.to change { order.events }\n");
@@ -230,6 +230,7 @@ pub fn check_multiline_bundle(
230
230
  /// | 25 | arguments_forwarding RedundantRestArgumentNames |
231
231
  /// | 26 | arguments_forwarding RedundantKeywordRestArgumentNames |
232
232
  /// | 27 | arguments_forwarding RedundantBlockArgumentNames |
233
+ /// | 28 | duplicate_methods_delegating (`Lint/DuplicateMethods` `DelegatingMethods`, 1.89) |
233
234
  ///
234
235
  /// Performance segment `nums[1]` (the shirobai-performance plugin origin):
235
236
  ///
@@ -485,7 +486,7 @@ pub const ORIGIN_RAILS: usize = 3;
485
486
  pub const N_ORIGINS: usize = 4;
486
487
 
487
488
  const CORE_NUMS_LEN: usize = 134;
488
- const CORE_LISTS_LEN: usize = 28;
489
+ const CORE_LISTS_LEN: usize = 29;
489
490
  const PERF_NUMS_LEN: usize = 3;
490
491
  const PERF_LISTS_LEN: usize = 1;
491
492
 
@@ -546,7 +547,7 @@ impl BundleConfig {
546
547
  }
547
548
  let mut lists = core_lists.into_iter();
548
549
  let mut next_list = || lists.next().expect("length checked above");
549
- Ok(BundleConfig {
550
+ let mut bundle = BundleConfig {
550
551
  debugger_methods: next_list(),
551
552
  debugger_requires: next_list(),
552
553
  block_length_max: nums[0] as usize,
@@ -802,6 +803,9 @@ impl BundleConfig {
802
803
  },
803
804
  duplicate_methods: duplicate_methods::Config {
804
805
  active_support_extensions_enabled: nums[111] != 0,
806
+ // Filled after the literal: list 28 sits at the end of the
807
+ // packing order, after the arguments_forwarding lists.
808
+ delegating_methods: Vec::new(),
805
809
  },
806
810
  redundant_freeze_target_30_plus: nums[114] != 0,
807
811
  redundant_freeze_string_literals_frozen_by_default: nums[115] != 0,
@@ -856,7 +860,9 @@ impl BundleConfig {
856
860
  // The shirobai-rails origin, read from its own segment (nums and
857
861
  // list lengths are checked there).
858
862
  rails: rails_config::RailsConfig::from_segment(rails_nums, &rails_lists)?,
859
- })
863
+ };
864
+ bundle.duplicate_methods.delegating_methods = next_list();
865
+ Ok(bundle)
860
866
  }
861
867
  }
862
868
 
@@ -2021,6 +2027,7 @@ mod tests {
2021
2027
  ["args", "arguments"].map(String::from).to_vec(), // af: RedundantRestArgumentNames
2022
2028
  ["kwargs", "options", "opts"].map(String::from).to_vec(), // af: RedundantKeywordRestArgumentNames
2023
2029
  ["blk", "block", "proc"].map(String::from).to_vec(), // af: RedundantBlockArgumentNames
2030
+ vec!["delegate".to_string()], // duplicate_methods: DelegatingMethods
2024
2031
  ];
2025
2032
  // Performance segment (origin 1): enabled, with the SafeMultiline
2026
2033
  // defaults and RuboCop's default preferred method for Detect