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 +4 -4
- data/Cargo.lock +2 -2
- data/Cargo.toml +1 -1
- data/crates/shirobai-core/src/rules/ambiguous_block_association.rs +215 -1
- data/crates/shirobai-core/src/rules/bundle.rs +10 -3
- data/crates/shirobai-core/src/rules/duplicate_methods.rs +183 -42
- data/crates/shirobai-core/src/rules/hash_alignment.rs +60 -9
- data/crates/shirobai-core/src/rules/if_unless_modifier.rs +32 -5
- data/crates/shirobai-core/src/rules/multiline_method_call_indentation.rs +32 -7
- data/crates/shirobai-core/src/rules/variable_number.rs +7 -0
- data/crates/shirobai-core/src/rules/void.rs +30 -5
- data/ext/shirobai/src/lib.rs +67 -28
- data/lib/shirobai/cop/layout/indentation_width.rb +1 -1
- data/lib/shirobai/cop/layout/line_length.rb +93 -0
- data/lib/shirobai/cop/lint/ambiguous_block_association.rb +26 -10
- data/lib/shirobai/cop/lint/duplicate_methods.rb +113 -8
- data/lib/shirobai/cop/naming/predicate_prefix.rb +42 -3
- data/lib/shirobai/cop/style/if_unless_modifier.rb +36 -16
- data/lib/shirobai/cop/style/mutable_constant.rb +2 -0
- data/lib/shirobai/dispatch.rb +5 -6
- data/lib/shirobai/inject.rb +82 -36
- data/lib/shirobai/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dba7704fcfe48ea2b368a6eae1afadb52dbfaaa926c4388f9d74a038f64c8bdf
|
|
4
|
+
data.tar.gz: 17e02721945c202d5cd39487dc155b66c42180f21e6692e58c3c0a8818b2d246
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|