shirobai 2026.0905.1300 → 2026.0920.1100
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/bin/bundle_profile.rs +1 -1
- data/crates/shirobai-core/src/rules/aligner.rs +20 -1
- data/crates/shirobai-core/src/rules/argument_alignment.rs +35 -4
- data/crates/shirobai-core/src/rules/bundle.rs +4 -4
- data/crates/shirobai-core/src/rules/duplicate_magic_comment.rs +35 -7
- data/crates/shirobai-core/src/rules/duplicate_methods.rs +24 -1
- data/crates/shirobai-core/src/rules/else_alignment.rs +54 -0
- data/crates/shirobai-core/src/rules/extra_spacing.rs +11 -0
- data/crates/shirobai-core/src/rules/first_array_element_indentation.rs +43 -1
- data/crates/shirobai-core/src/rules/hash_syntax.rs +60 -6
- data/crates/shirobai-core/src/rules/indentation_width.rs +16 -1
- data/crates/shirobai-core/src/rules/line_length_breakable.rs +63 -8
- data/crates/shirobai-core/src/rules/ordered_magic_comments.rs +9 -0
- data/crates/shirobai-core/src/rules/parentheses_as_grouped_expression.rs +97 -0
- data/crates/shirobai-core/src/rules/space_around_operators.rs +30 -8
- data/ext/shirobai/src/lib.rs +6 -5
- data/lib/shirobai/cop/layout/argument_alignment.rb +11 -8
- data/lib/shirobai/cop/layout/hash_alignment.rb +11 -5
- 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: f65a6269e60ead3a7b5175cad5be36b3364bb5ee6025dd7bcc3a2b82d69466c6
|
|
4
|
+
data.tar.gz: 6af011435008fec04f1a8bced1d5e5d620160b465b6a290963fcfcda1561a8a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d88a4fc322d82e59683f33de8d8a3ae93a62f59ac358dc583a83201ccf3db4021a5076ac60a4a5ba60802d557ab8d18ad3771d6be40a1c01268582aa4083e743
|
|
7
|
+
data.tar.gz: 2cee35078890630d5291e962e5fb6d6a9c2f49a4595bd1b212ead6db8aaff8a73aa8460845e11d49d2360d26680d2006bbde79b2ba7c2646e6fbbbefa2b48032
|
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.920.1100"
|
|
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.920.1100"
|
|
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.920.1100"
|
|
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
|
|
@@ -213,7 +213,7 @@ fn run_perrule(sources: &[Vec<u8>], cfg: &BundleConfig, iterations: usize) {
|
|
|
213
213
|
s,
|
|
214
214
|
cfg.argument_alignment_style,
|
|
215
215
|
cfg.argument_alignment_indent,
|
|
216
|
-
cfg.
|
|
216
|
+
cfg.argument_alignment_hash_separator,
|
|
217
217
|
));
|
|
218
218
|
}),
|
|
219
219
|
e!("first_argument_indentation", |s: &[u8]| {
|
|
@@ -389,6 +389,7 @@ impl<'a> Aligner<'a> {
|
|
|
389
389
|
let original_line_indent = self.line_indentation(first);
|
|
390
390
|
let mut relevant_line_indent_at_level = true;
|
|
391
391
|
let assignment_lines = self.assignment_lines();
|
|
392
|
+
let boundary_lines = self.interrupting_operator_lines(&assignment_lines);
|
|
392
393
|
|
|
393
394
|
for &line_number in line_range {
|
|
394
395
|
let current_line_indent = self.line_indentation(line_number);
|
|
@@ -396,7 +397,11 @@ impl<'a> Aligner<'a> {
|
|
|
396
397
|
.line_text(line_number)
|
|
397
398
|
.map(is_blank_line)
|
|
398
399
|
.unwrap_or(true);
|
|
399
|
-
|
|
400
|
+
// A line carrying another alignable operator (`<<`, `==`, ...)
|
|
401
|
+
// that is not an assignment line ends the group (1.91,
|
|
402
|
+
// `interrupting_operator_lines` as `relevant_lines` boundaries).
|
|
403
|
+
if (line_number != first && boundary_lines.contains(&line_number))
|
|
404
|
+
|| (current_line_indent < original_line_indent && !blank_line)
|
|
400
405
|
|| (relevant_line_indent_at_level && blank_line)
|
|
401
406
|
{
|
|
402
407
|
break;
|
|
@@ -413,6 +418,20 @@ impl<'a> Aligner<'a> {
|
|
|
413
418
|
result
|
|
414
419
|
}
|
|
415
420
|
|
|
421
|
+
/// `interrupting_operator_lines`: the lines of every
|
|
422
|
+
/// `ASSIGNMENT_OR_COMPARISON_TOKENS` token that are not assignment lines.
|
|
423
|
+
fn interrupting_operator_lines(
|
|
424
|
+
&self,
|
|
425
|
+
assignment_lines: &[usize],
|
|
426
|
+
) -> std::collections::BTreeSet<usize> {
|
|
427
|
+
self.tokens
|
|
428
|
+
.iter()
|
|
429
|
+
.filter(|t| t.assignment_or_comparison())
|
|
430
|
+
.map(|t| self.line(t.begin_pos))
|
|
431
|
+
.filter(|line| !assignment_lines.contains(line))
|
|
432
|
+
.collect()
|
|
433
|
+
}
|
|
434
|
+
|
|
416
435
|
/// `assignment_lines` = lines of `assignment_tokens`.
|
|
417
436
|
fn assignment_lines(&self) -> Vec<usize> {
|
|
418
437
|
self.assignment_tokens()
|
|
@@ -36,9 +36,9 @@ pub fn check_argument_alignment(
|
|
|
36
36
|
source: &[u8],
|
|
37
37
|
style: u8,
|
|
38
38
|
indent_width: usize,
|
|
39
|
-
|
|
39
|
+
hash_separator: bool,
|
|
40
40
|
) -> Vec<ArgAlignOffense> {
|
|
41
|
-
let Some(mut rule) = build_rule(source, style, indent_width,
|
|
41
|
+
let Some(mut rule) = build_rule(source, style, indent_width, hash_separator) else {
|
|
42
42
|
return Vec::new();
|
|
43
43
|
};
|
|
44
44
|
super::dispatch::run(source, &mut [&mut rule]);
|
|
@@ -47,11 +47,15 @@ pub fn check_argument_alignment(
|
|
|
47
47
|
|
|
48
48
|
/// Build the rule for use standalone or in a shared-walk bundle. `None` when
|
|
49
49
|
/// the cop is disabled outright (`autocorrect_incompatible_with_other_cops?`).
|
|
50
|
+
/// `hash_separator` is `enforce_hash_argument_with_separator?`: a
|
|
51
|
+
/// separator-aligned `Layout/HashAlignment`. With `with_first_argument` it
|
|
52
|
+
/// disables the cop; with `with_fixed_indentation` it leaves a trailing
|
|
53
|
+
/// braceless hash's pairs to HashAlignment (1.91).
|
|
50
54
|
pub(crate) fn build_rule(
|
|
51
55
|
source: &[u8],
|
|
52
56
|
style: u8,
|
|
53
57
|
indent_width: usize,
|
|
54
|
-
|
|
58
|
+
hash_separator: bool,
|
|
55
59
|
) -> Option<Visitor<'_>> {
|
|
56
60
|
let style = if style == 1 {
|
|
57
61
|
Style::WithFixedIndentation
|
|
@@ -59,7 +63,7 @@ pub(crate) fn build_rule(
|
|
|
59
63
|
Style::WithFirstArgument
|
|
60
64
|
};
|
|
61
65
|
// `autocorrect_incompatible_with_other_cops?`: disables the cop entirely.
|
|
62
|
-
if
|
|
66
|
+
if hash_separator && style == Style::WithFirstArgument {
|
|
63
67
|
return None;
|
|
64
68
|
}
|
|
65
69
|
let line_index = super::line_index::with_line_index(source, |li| li.clone());
|
|
@@ -68,6 +72,7 @@ pub(crate) fn build_rule(
|
|
|
68
72
|
line_index,
|
|
69
73
|
style,
|
|
70
74
|
indent: indent_width,
|
|
75
|
+
hash_separator,
|
|
71
76
|
offenses: Vec::new(),
|
|
72
77
|
})
|
|
73
78
|
}
|
|
@@ -77,6 +82,8 @@ pub(crate) struct Visitor<'a> {
|
|
|
77
82
|
line_index: Rc<LineIndex>,
|
|
78
83
|
style: Style,
|
|
79
84
|
indent: usize,
|
|
85
|
+
/// `enforce_hash_argument_with_separator?`.
|
|
86
|
+
hash_separator: bool,
|
|
80
87
|
pub(crate) offenses: Vec<ArgAlignOffense>,
|
|
81
88
|
}
|
|
82
89
|
|
|
@@ -170,6 +177,10 @@ impl Visitor<'_> {
|
|
|
170
177
|
.collect();
|
|
171
178
|
let last = args.last().unwrap();
|
|
172
179
|
match braceless_hash_pairs(last) {
|
|
180
|
+
// `return items if enforce_hash_argument_with_separator?`
|
|
181
|
+
// (1.91): the pairs belong to HashAlignment's separator
|
|
182
|
+
// alignment, so they are not alignment items here.
|
|
183
|
+
Some(_) if self.hash_separator => {}
|
|
173
184
|
Some(pairs) => items.extend(pairs),
|
|
174
185
|
None => items.push(loc(&last.location())),
|
|
175
186
|
}
|
|
@@ -306,4 +317,24 @@ mod tests {
|
|
|
306
317
|
assert_eq!(got.len(), 1);
|
|
307
318
|
assert_eq!(got[0].2, 3); // `bar` (col 2) aligns under `foo` (col 5)
|
|
308
319
|
}
|
|
320
|
+
|
|
321
|
+
// 1.91: with a separator-aligned `Layout/HashAlignment`, the pairs of a
|
|
322
|
+
// trailing braceless hash are not alignment items under
|
|
323
|
+
// `with_fixed_indentation` (they belong to HashAlignment); other
|
|
324
|
+
// arguments are still checked. Under `with_first_argument` the cop is
|
|
325
|
+
// disabled outright.
|
|
326
|
+
#[test]
|
|
327
|
+
fn hash_separator_leaves_trailing_hash_pairs_to_hash_alignment() {
|
|
328
|
+
let sep = |src: &str, style: u8| {
|
|
329
|
+
check_argument_alignment(src.as_bytes(), style, 2, true)
|
|
330
|
+
.into_iter()
|
|
331
|
+
.map(|o| o.column_delta)
|
|
332
|
+
.collect::<Vec<_>>()
|
|
333
|
+
};
|
|
334
|
+
assert!(sep("validates :foo,\n bar: 1,\n bazqux: 2\n", 1).is_empty());
|
|
335
|
+
assert_eq!(sep("func(:foo,\n :bar)\n", 1), vec![-3]);
|
|
336
|
+
assert!(sep("func(a,\n b,\nc)\n", 0).is_empty());
|
|
337
|
+
// Without the separator style the pairs are aligned as before.
|
|
338
|
+
assert_eq!(run("validates :foo,\n bar: 1,\n bazqux: 2\n", 1).len(), 2);
|
|
339
|
+
}
|
|
309
340
|
}
|
|
@@ -332,7 +332,7 @@ pub struct BundleConfig {
|
|
|
332
332
|
pub multiline_method_call: (u8, usize, usize),
|
|
333
333
|
pub argument_alignment_style: u8,
|
|
334
334
|
pub argument_alignment_indent: usize,
|
|
335
|
-
pub
|
|
335
|
+
pub argument_alignment_hash_separator: bool,
|
|
336
336
|
pub array_alignment_style: u8,
|
|
337
337
|
pub array_alignment_indent: usize,
|
|
338
338
|
pub first_argument_style: u8,
|
|
@@ -579,7 +579,7 @@ impl BundleConfig {
|
|
|
579
579
|
multiline_method_call: (nums[18] as u8, nums[19] as usize, nums[20] as usize),
|
|
580
580
|
argument_alignment_style: nums[21] as u8,
|
|
581
581
|
argument_alignment_indent: nums[22] as usize,
|
|
582
|
-
|
|
582
|
+
argument_alignment_hash_separator: nums[23] != 0,
|
|
583
583
|
array_alignment_style: nums[112] as u8,
|
|
584
584
|
array_alignment_indent: nums[113] as usize,
|
|
585
585
|
frozen_string_literal_comment_style: nums[116] as u8,
|
|
@@ -1212,7 +1212,7 @@ pub fn check_all_bundle(source: &[u8], cfg: &BundleConfig) -> BundleResult {
|
|
|
1212
1212
|
source,
|
|
1213
1213
|
cfg.argument_alignment_style,
|
|
1214
1214
|
cfg.argument_alignment_indent,
|
|
1215
|
-
cfg.
|
|
1215
|
+
cfg.argument_alignment_hash_separator,
|
|
1216
1216
|
);
|
|
1217
1217
|
let mut ara_rule = array_alignment::build_rule(
|
|
1218
1218
|
source,
|
|
@@ -4486,7 +4486,7 @@ mod tests {
|
|
|
4486
4486
|
fn shared_walk_respects_disabled_rules() {
|
|
4487
4487
|
let src = "foo(bar,\n baz)\nfoo([\n 1\n])\n";
|
|
4488
4488
|
let (mut nums, lists) = default_packed();
|
|
4489
|
-
nums[0][23] = 1; // argument_alignment
|
|
4489
|
+
nums[0][23] = 1; // argument_alignment hash_separator (disables with_first_argument)
|
|
4490
4490
|
nums[0][26] = 1; // first_argument enforce_fixed_no_line_break
|
|
4491
4491
|
nums[0][37] = 1; // first_array_element enforce_fixed_indentation
|
|
4492
4492
|
let cfg = BundleConfig::from_packed(&nums, lists).unwrap();
|
|
@@ -730,9 +730,9 @@ fn simple_fsl_value(line: &[u8]) -> Option<FslValue> {
|
|
|
730
730
|
/// is already false: `valid?` = `@comment.start_with?('#')` (no leading
|
|
731
731
|
/// space) AND `any?` = one of the remaining magic kinds is specified
|
|
732
732
|
/// (`frozen_string_literal`, `shareable_constant_value`, `rbs_inline`,
|
|
733
|
-
/// `typed`). `rbs_inline` is special: it counts
|
|
734
|
-
/// exactly `enabled`/`disabled`
|
|
735
|
-
/// `valid_rbs_inline_value?`).
|
|
733
|
+
/// `warn_indent` since 1.91, `typed`). `rbs_inline` is special: it counts
|
|
734
|
+
/// only when the value is exactly `enabled`/`disabled`
|
|
735
|
+
/// (`rbs_inline_specified?` == `valid_rbs_inline_value?`).
|
|
736
736
|
/// - [`OrderedBucket::None`] — neither.
|
|
737
737
|
///
|
|
738
738
|
/// The format dispatch mirrors `MagicComment.parse` (Emacs, else Vim, else
|
|
@@ -750,15 +750,19 @@ pub(crate) fn ordered_bucket(line: &[u8]) -> OrderedBucket {
|
|
|
750
750
|
let hash_prefixed = line.first() == Some(&b'#');
|
|
751
751
|
|
|
752
752
|
if let Some(token) = emacs_token(line) {
|
|
753
|
-
// EmacsComment: `;`-separated tokens, each stripped. `encoding
|
|
754
|
-
// `frozen_string_literal
|
|
755
|
-
// Emacs can carry (`rbs_inline`/`typed` extraction
|
|
753
|
+
// EmacsComment: `;`-separated tokens, each stripped. `encoding`,
|
|
754
|
+
// `frozen_string_literal`, `shareable_constant_value` and `warn_indent`
|
|
755
|
+
// are the only kinds Emacs can carry (`rbs_inline`/`typed` extraction
|
|
756
|
+
// is nil).
|
|
756
757
|
let mut encoding = false;
|
|
757
758
|
let mut other = false;
|
|
758
759
|
for tok in split_strip(token, b";") {
|
|
759
760
|
if editor_kv_matches(tok, &[b"encoding", b"coding"], b':') {
|
|
760
761
|
encoding = true;
|
|
761
|
-
} else if fsl_keyword_kv_matches(tok)
|
|
762
|
+
} else if fsl_keyword_kv_matches(tok)
|
|
763
|
+
|| shareable_keyword_kv_matches(tok)
|
|
764
|
+
|| warn_indent_keyword_kv_matches(tok)
|
|
765
|
+
{
|
|
762
766
|
other = true;
|
|
763
767
|
}
|
|
764
768
|
}
|
|
@@ -790,6 +794,7 @@ pub(crate) fn ordered_bucket(line: &[u8]) -> OrderedBucket {
|
|
|
790
794
|
&& (simple_fsl(line)
|
|
791
795
|
|| simple_shareable_specified(line)
|
|
792
796
|
|| simple_rbs_inline_specified(line)
|
|
797
|
+
|| simple_warn_indent_specified(line)
|
|
793
798
|
|| simple_typed_specified(line))
|
|
794
799
|
{
|
|
795
800
|
return OrderedBucket::OtherValid;
|
|
@@ -797,6 +802,29 @@ pub(crate) fn ordered_bucket(line: &[u8]) -> OrderedBucket {
|
|
|
797
802
|
OrderedBucket::None
|
|
798
803
|
}
|
|
799
804
|
|
|
805
|
+
/// Emacs editor token match for `\Awarn[_-]indent\s*:\s*TOKEN\z`
|
|
806
|
+
/// (case-SENSITIVE keyword, like `EditorComment#match`). `warn_indent` is a
|
|
807
|
+
/// magic comment since 1.91 (`MagicComment::KEYWORDS[:warn_indent]`).
|
|
808
|
+
fn warn_indent_keyword_kv_matches(tok: &[u8]) -> bool {
|
|
809
|
+
match warn_indent_keyword_len(tok, false) {
|
|
810
|
+
Some(klen) => editor_kv_matches(&tok[klen..], &[b""], b':'),
|
|
811
|
+
None => false,
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/// Length of `warn[_-]indent` at the start of `s`, or None.
|
|
816
|
+
fn warn_indent_keyword_len(s: &[u8], ci: bool) -> Option<usize> {
|
|
817
|
+
let mut p = eat(s, 0, b"warn", ci)?;
|
|
818
|
+
p = eat_one_of(s, p, b"_-")?;
|
|
819
|
+
p = eat(s, p, b"indent", ci)?;
|
|
820
|
+
Some(p)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/// `warn_indent_specified?` for a SimpleComment line (any TOKEN).
|
|
824
|
+
fn simple_warn_indent_specified(line: &[u8]) -> bool {
|
|
825
|
+
simple_anchored_token(line, |s| warn_indent_keyword_len(s, true)).is_some()
|
|
826
|
+
}
|
|
827
|
+
|
|
800
828
|
/// Emacs editor token match for `\Ashareable[_-]constant[_-]value\s*:\s*TOKEN\z`
|
|
801
829
|
/// (case-SENSITIVE keyword, like `EditorComment#match`).
|
|
802
830
|
fn shareable_keyword_kv_matches(tok: &[u8]) -> bool {
|
|
@@ -223,6 +223,8 @@ enum Kind {
|
|
|
223
223
|
chain: Option<ConstChain>,
|
|
224
224
|
},
|
|
225
225
|
Lvasgn,
|
|
226
|
+
/// `@ivar = Class.new do ... end`: an `:ivasgn` parent (1.91).
|
|
227
|
+
Ivasgn,
|
|
226
228
|
If,
|
|
227
229
|
Other,
|
|
228
230
|
}
|
|
@@ -491,6 +493,7 @@ enum PParent<'a> {
|
|
|
491
493
|
BeginWrap { pp_block: bool },
|
|
492
494
|
Def(usize),
|
|
493
495
|
Casgn,
|
|
496
|
+
Ivasgn,
|
|
494
497
|
Lvasgn,
|
|
495
498
|
Disallowed,
|
|
496
499
|
}
|
|
@@ -564,6 +567,7 @@ impl<'s> DuplicateMethodsRule<'s> {
|
|
|
564
567
|
}
|
|
565
568
|
Kind::Call(info) => PParent::Call(info),
|
|
566
569
|
Kind::Casgn { .. } => PParent::Casgn,
|
|
570
|
+
Kind::Ivasgn => PParent::Ivasgn,
|
|
567
571
|
Kind::Lvasgn => PParent::Lvasgn,
|
|
568
572
|
Kind::Def { .. } => PParent::Def(call_idx - 1),
|
|
569
573
|
_ => PParent::Disallowed,
|
|
@@ -633,7 +637,9 @@ impl<'s> DuplicateMethodsRule<'s> {
|
|
|
633
637
|
None => path_line(),
|
|
634
638
|
}
|
|
635
639
|
}
|
|
636
|
-
|
|
640
|
+
// `:casgn` and `:ivasgn` (1.91) parents have no receiver; the
|
|
641
|
+
// block gets its unique source-location id.
|
|
642
|
+
PParent::Casgn | PParent::Ivasgn => path_line(),
|
|
637
643
|
PParent::BeginWrap { pp_block } => {
|
|
638
644
|
if pp_block {
|
|
639
645
|
path_line()
|
|
@@ -1244,6 +1250,7 @@ impl<'s> DuplicateMethodsRule<'s> {
|
|
|
1244
1250
|
}
|
|
1245
1251
|
}
|
|
1246
1252
|
Node::LocalVariableWriteNode { .. } => frame.kind = Kind::Lvasgn,
|
|
1253
|
+
Node::InstanceVariableWriteNode { .. } => frame.kind = Kind::Ivasgn,
|
|
1247
1254
|
Node::CallNode { .. } => {
|
|
1248
1255
|
let c = node.as_call_node().unwrap();
|
|
1249
1256
|
let kind = block_kind(&c);
|
|
@@ -1905,4 +1912,20 @@ mod tests {
|
|
|
1905
1912
|
);
|
|
1906
1913
|
assert_eq!(&src[evts[0].off_start..evts[0].off_end], "alias_method :a, :b");
|
|
1907
1914
|
}
|
|
1915
|
+
|
|
1916
|
+
// 1.91: a Class.new block assigned to an instance variable (`:ivasgn`
|
|
1917
|
+
// parent) gets its own path:line scope id like a casgn one, so singleton
|
|
1918
|
+
// methods in separate blocks are not duplicates of each other.
|
|
1919
|
+
#[test]
|
|
1920
|
+
fn ivasgn_class_new_blocks_get_distinct_scope_ids() {
|
|
1921
|
+
let src = "def setup\n @first = Class.new do\n class << self\n def name; end\n end\n end\n\n Class.new do\n class << self\n def name; end\n end\n end\nend\n";
|
|
1922
|
+
let evts = events(src);
|
|
1923
|
+
// `def setup`, then the two singleton `name`s.
|
|
1924
|
+
assert_eq!(evts.len(), 3);
|
|
1925
|
+
assert_eq!(evts[1].0, evts[2].0);
|
|
1926
|
+
assert_eq!(evts[1].2, 2); // @path:2 (the ivasgn block)
|
|
1927
|
+
// The second block's parent is the def body's `begin`: no scope id,
|
|
1928
|
+
// as before.
|
|
1929
|
+
assert_eq!(evts[2].2, -1);
|
|
1930
|
+
}
|
|
1908
1931
|
}
|
|
@@ -114,6 +114,9 @@ enum FrameKind {
|
|
|
114
114
|
},
|
|
115
115
|
/// A block node (`BlockNode`).
|
|
116
116
|
Block,
|
|
117
|
+
/// A `class` / `module` / `class << self` body: the opening keyword range.
|
|
118
|
+
/// An implicit-begin `rescue`/`else` in such a body aligns with it (1.91).
|
|
119
|
+
ClassLike { kw_start: usize, kw_end: usize },
|
|
117
120
|
/// A write node (assignment LHS): the LHS span (start..value_start trimmed),
|
|
118
121
|
/// here approximated by the whole write-node range start and its first line.
|
|
119
122
|
Write,
|
|
@@ -413,6 +416,13 @@ impl<'a> Visitor<'a> {
|
|
|
413
416
|
let call_start = self.frames[nf - 2].start;
|
|
414
417
|
return self.start_line_range(call_start);
|
|
415
418
|
}
|
|
419
|
+
// `when :class, :module, :sclass then parent.loc.keyword` (1.91).
|
|
420
|
+
FrameKind::ClassLike { kw_start, kw_end } => {
|
|
421
|
+
return AlignRange {
|
|
422
|
+
start: *kw_start,
|
|
423
|
+
end: *kw_end,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
416
426
|
_ => {}
|
|
417
427
|
}
|
|
418
428
|
}
|
|
@@ -670,6 +680,24 @@ impl<'a> Visitor<'a> {
|
|
|
670
680
|
}
|
|
671
681
|
} else if node.as_block_node().is_some() {
|
|
672
682
|
FrameKind::Block
|
|
683
|
+
} else if let Some(n) = node.as_class_node() {
|
|
684
|
+
let (s, e) = loc(&n.class_keyword_loc());
|
|
685
|
+
FrameKind::ClassLike {
|
|
686
|
+
kw_start: s,
|
|
687
|
+
kw_end: e,
|
|
688
|
+
}
|
|
689
|
+
} else if let Some(n) = node.as_module_node() {
|
|
690
|
+
let (s, e) = loc(&n.module_keyword_loc());
|
|
691
|
+
FrameKind::ClassLike {
|
|
692
|
+
kw_start: s,
|
|
693
|
+
kw_end: e,
|
|
694
|
+
}
|
|
695
|
+
} else if let Some(n) = node.as_singleton_class_node() {
|
|
696
|
+
let (s, e) = loc(&n.class_keyword_loc());
|
|
697
|
+
FrameKind::ClassLike {
|
|
698
|
+
kw_start: s,
|
|
699
|
+
kw_end: e,
|
|
700
|
+
}
|
|
673
701
|
} else if is_write_node(node) {
|
|
674
702
|
FrameKind::Write
|
|
675
703
|
} else {
|
|
@@ -843,4 +871,30 @@ mod tests {
|
|
|
843
871
|
fn rescue_inside_if_branches_no_offense() {
|
|
844
872
|
assert!(variable("if a\n a rescue nil\nelse\n a rescue nil\nend\n").is_empty());
|
|
845
873
|
}
|
|
874
|
+
|
|
875
|
+
// 1.91: an implicit-begin `rescue`/`else` in a class body aligns with
|
|
876
|
+
// the `class` keyword (`when :class, :module, :sclass then
|
|
877
|
+
// parent.loc.keyword`), not with `rescue`.
|
|
878
|
+
#[test]
|
|
879
|
+
fn class_body_else_aligns_with_class_keyword() {
|
|
880
|
+
let r = keyword("class MyClass\n foo\nrescue SomeException\n bar\n else\n baz\nend\n");
|
|
881
|
+
assert_eq!(r.len(), 1);
|
|
882
|
+
assert!(r[0].message.contains("Align `else` with `class`."));
|
|
883
|
+
assert_eq!(r[0].column_delta, -2);
|
|
884
|
+
assert!(keyword("class MyClass\n foo\nrescue SomeException\n bar\nelse\n baz\nend\n").is_empty());
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
#[test]
|
|
888
|
+
fn module_body_else_aligns_with_module_keyword() {
|
|
889
|
+
let r = keyword("module M\n foo\nrescue E\n bar\n else\n baz\nend\n");
|
|
890
|
+
assert_eq!(r.len(), 1);
|
|
891
|
+
assert!(r[0].message.contains("Align `else` with `module`."));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#[test]
|
|
895
|
+
fn sclass_body_else_aligns_with_class_keyword() {
|
|
896
|
+
let r = keyword("class << self\n foo\nrescue E\n bar\n else\n baz\nensure\n q\nend\n");
|
|
897
|
+
assert_eq!(r.len(), 1);
|
|
898
|
+
assert!(r[0].message.contains("Align `else` with `class`."));
|
|
899
|
+
}
|
|
846
900
|
}
|
|
@@ -1050,4 +1050,15 @@ mod tests {
|
|
|
1050
1050
|
// The first `=` (col 2) is not aligned with the block's max -> flagged.
|
|
1051
1051
|
assert_eq!(off.len(), 1);
|
|
1052
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
// 1.91: a line carrying another alignable operator (`<<`, `==`, ...)
|
|
1055
|
+
// that is not itself an assignment line ends the equal-sign alignment
|
|
1056
|
+
// group, so the assignments around it are not "unaligned".
|
|
1057
|
+
#[test]
|
|
1058
|
+
fn force_alignment_group_ends_at_an_interrupting_operator_line() {
|
|
1059
|
+
assert!(run("aaaa = b\ne << f\ng += h\n", cfg(true, false, true)).is_empty());
|
|
1060
|
+
assert!(run("aaaa = b\nraise if e == f\ng += h\n", cfg(true, false, true)).is_empty());
|
|
1061
|
+
// Sanity: adjacent assignments still form a group.
|
|
1062
|
+
assert_eq!(run("aaaa = b\ng += h\n", cfg(true, false, true)).len(), 1);
|
|
1063
|
+
}
|
|
1053
1064
|
}
|
|
@@ -86,7 +86,8 @@ pub fn check_first_array_element_indentation(
|
|
|
86
86
|
/// Build the rule for use standalone or in a shared-walk bundle. `None` when
|
|
87
87
|
/// the cop is disabled outright: `Layout/ArrayAlignment` enforcing
|
|
88
88
|
/// `with_fixed_indentation` gates both `on_array` and `on_send` unless the
|
|
89
|
-
/// style is `consistent
|
|
89
|
+
/// style is `consistent` (`autocorrect_incompatible_with_other_cops?`, whose
|
|
90
|
+
/// per-array part for the `consistent` style runs in `process_array`).
|
|
90
91
|
pub(crate) fn build_rule(
|
|
91
92
|
source: &[u8],
|
|
92
93
|
style: u8,
|
|
@@ -102,6 +103,7 @@ pub(crate) fn build_rule(
|
|
|
102
103
|
line_index: super::line_index::with_line_index(source, |li| li.clone()),
|
|
103
104
|
style,
|
|
104
105
|
indent: indent_width,
|
|
106
|
+
enforce_fixed_indentation,
|
|
105
107
|
stack: Vec::new(),
|
|
106
108
|
offenses: Vec::new(),
|
|
107
109
|
})
|
|
@@ -166,6 +168,10 @@ pub(crate) struct Visitor<'a> {
|
|
|
166
168
|
line_index: Rc<LineIndex>,
|
|
167
169
|
style: Style,
|
|
168
170
|
indent: usize,
|
|
171
|
+
/// `enforce_first_argument_with_fixed_indentation?` (`Layout/ArrayAlignment`
|
|
172
|
+
/// enforces `with_fixed_indentation`). Only reaches the visitor with the
|
|
173
|
+
/// `consistent` style; the other styles stand down in `build_rule`.
|
|
174
|
+
enforce_fixed_indentation: bool,
|
|
169
175
|
stack: Vec<Frame>,
|
|
170
176
|
pub(crate) offenses: Vec<FirstArrayElemIndentOffense>,
|
|
171
177
|
}
|
|
@@ -313,6 +319,21 @@ impl<'a> Visitor<'a> {
|
|
|
313
319
|
let paren = self.claimed_paren(open_start);
|
|
314
320
|
let first = a.elements().iter().next().map(|e| loc(&e.location()));
|
|
315
321
|
|
|
322
|
+
// `autocorrect_incompatible_with_other_cops?` for the `consistent`
|
|
323
|
+
// style (1.91): under `Layout/ArrayAlignment: with_fixed_indentation`
|
|
324
|
+
// an array of two or more elements is left alone unless its indent
|
|
325
|
+
// base is the start of its line (ArrayAlignment does not align
|
|
326
|
+
// single-element arrays, so those are still checked).
|
|
327
|
+
if self.enforce_fixed_indentation
|
|
328
|
+
&& a.elements().iter().count() >= 2
|
|
329
|
+
&& !matches!(
|
|
330
|
+
self.indent_base(open_start, first, paren),
|
|
331
|
+
(_, BaseType::StartOfLine)
|
|
332
|
+
)
|
|
333
|
+
{
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
316
337
|
if let Some(f) = first {
|
|
317
338
|
if self.line_of(f.0) == self.line_of(open_start) {
|
|
318
339
|
return;
|
|
@@ -640,4 +661,25 @@ mod tests {
|
|
|
640
661
|
.is_empty()
|
|
641
662
|
);
|
|
642
663
|
}
|
|
664
|
+
|
|
665
|
+
// 1.91 `autocorrect_incompatible_with_other_cops?` for the `consistent`
|
|
666
|
+
// style under `Layout/ArrayAlignment: with_fixed_indentation`: an array of
|
|
667
|
+
// two or more elements whose indent base is not the start of its line is
|
|
668
|
+
// left to ArrayAlignment; single-element and bracketless arrays are not.
|
|
669
|
+
#[test]
|
|
670
|
+
fn consistent_style_defers_multi_element_arrays_to_array_alignment() {
|
|
671
|
+
let enforced = |src: &str| {
|
|
672
|
+
check_first_array_element_indentation(src.as_bytes(), 1, 2, true)
|
|
673
|
+
.into_iter()
|
|
674
|
+
.map(|o| o.message)
|
|
675
|
+
.collect::<Vec<_>>()
|
|
676
|
+
};
|
|
677
|
+
assert!(enforced("foo bar: [\n 'foo',\n 'bar'\n],\nbaz: 'baz'\n").is_empty());
|
|
678
|
+
let single = enforced("foo bar: [\n1\n],\nbaz: 3\n");
|
|
679
|
+
assert_eq!(single.len(), 2);
|
|
680
|
+
assert!(single[0].contains("relative to the parent hash key"));
|
|
681
|
+
assert!(enforced("x = 1,\n 2\n").is_empty());
|
|
682
|
+
// A start-of-line base is still checked (the bracket itself is fine).
|
|
683
|
+
assert_eq!(enforced("x = [\n1,\n2\n]\n").len(), 1);
|
|
684
|
+
}
|
|
643
685
|
}
|
|
@@ -151,6 +151,10 @@ enum FrameKind {
|
|
|
151
151
|
braces: bool,
|
|
152
152
|
/// `pairs.last` range and whether `key.source == value.source` (omittable).
|
|
153
153
|
last_pair: Option<((usize, usize), bool)>,
|
|
154
|
+
/// `hash_rockets_enforced?(hash)` (1.91): the whole hash converts to
|
|
155
|
+
/// rockets (`EnforcedStyle: hash_rockets` or `force_hash_rockets?`),
|
|
156
|
+
/// so its pairs skip the shorthand checks.
|
|
157
|
+
rockets_enforced: bool,
|
|
154
158
|
},
|
|
155
159
|
/// Prism `ArgumentsNode`: transparent in parser, skipped when finding a
|
|
156
160
|
/// node's "parser parent".
|
|
@@ -334,11 +338,25 @@ impl Visitor<'_> {
|
|
|
334
338
|
let h = node.as_keyword_hash_node()?;
|
|
335
339
|
(h.elements().iter().collect(), false)
|
|
336
340
|
};
|
|
337
|
-
let
|
|
338
|
-
|
|
339
|
-
(
|
|
340
|
-
|
|
341
|
-
|
|
341
|
+
let pairs: Vec<Pair> = elements
|
|
342
|
+
.iter()
|
|
343
|
+
.filter_map(|e| e.as_assoc_node())
|
|
344
|
+
.map(|a| pair_from_assoc(self.source, &a))
|
|
345
|
+
.collect();
|
|
346
|
+
let last_pair = pairs
|
|
347
|
+
.last()
|
|
348
|
+
.map(|p| (p.range, p.key_source == p.value_source));
|
|
349
|
+
let rockets_enforced = self.hash_rockets_enforced(&pairs);
|
|
350
|
+
Some(FrameKind::Hash {
|
|
351
|
+
braces,
|
|
352
|
+
last_pair,
|
|
353
|
+
rockets_enforced,
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// `hash_rockets_enforced?(hash_node)` (1.91).
|
|
358
|
+
fn hash_rockets_enforced(&self, pairs: &[Pair]) -> bool {
|
|
359
|
+
self.cfg.style == STYLE_HASH_ROCKETS || self.force_hash_rockets(pairs)
|
|
342
360
|
}
|
|
343
361
|
|
|
344
362
|
fn call_frame(&self, node: &Node<'_>) -> Option<CallFrame> {
|
|
@@ -557,12 +575,16 @@ impl Visitor<'_> {
|
|
|
557
575
|
// ---- shorthand: `on_hash_for_mixed_shorthand` (consistent / either_consistent) ----
|
|
558
576
|
|
|
559
577
|
fn on_hash_for_mixed_shorthand(&mut self, pairs: &[Pair]) {
|
|
578
|
+
// `ignore_mixed_hash_shorthand_syntax?`.
|
|
560
579
|
if !self.cfg.ruby31_plus {
|
|
561
580
|
return;
|
|
562
581
|
}
|
|
563
582
|
if self.cfg.shorthand != SHORT_CONSISTENT && self.cfg.shorthand != SHORT_EITHER_CONSISTENT {
|
|
564
583
|
return;
|
|
565
584
|
}
|
|
585
|
+
if self.hash_rockets_enforced(pairs) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
566
588
|
let mut omitted = Vec::new();
|
|
567
589
|
let mut omittable = Vec::new();
|
|
568
590
|
let mut needed = false;
|
|
@@ -618,6 +640,17 @@ impl Visitor<'_> {
|
|
|
618
640
|
if self.ignore_hash_shorthand_syntax() {
|
|
619
641
|
return;
|
|
620
642
|
}
|
|
643
|
+
// `hash_rockets_enforced?(pair_node.parent)` (1.91): the parent hash
|
|
644
|
+
// is the stack top when `on_pair` runs from `enter(AssocNode)`.
|
|
645
|
+
if matches!(
|
|
646
|
+
self.stack.last().map(|f| &f.kind),
|
|
647
|
+
Some(FrameKind::Hash {
|
|
648
|
+
rockets_enforced: true,
|
|
649
|
+
..
|
|
650
|
+
})
|
|
651
|
+
) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
621
654
|
if self.cfg.shorthand == SHORT_ALWAYS {
|
|
622
655
|
if pair.value_omission || self.require_hash_value(pair) {
|
|
623
656
|
return;
|
|
@@ -785,7 +818,9 @@ impl Visitor<'_> {
|
|
|
785
818
|
// return unless last_pair.key.source == last_pair.value.source
|
|
786
819
|
let hash_idx = self.stack.len().checked_sub(1)?;
|
|
787
820
|
let (hash_braces, last_pair) = match &self.stack[hash_idx].kind {
|
|
788
|
-
FrameKind::Hash {
|
|
821
|
+
FrameKind::Hash {
|
|
822
|
+
braces, last_pair, ..
|
|
823
|
+
} => (*braces, *last_pair),
|
|
789
824
|
_ => return None,
|
|
790
825
|
};
|
|
791
826
|
let (_last_pair_range, last_pair_omittable) = last_pair?;
|
|
@@ -1309,4 +1344,23 @@ mod tests {
|
|
|
1309
1344
|
assert!(offenses("{foo: foo}", &c).is_empty());
|
|
1310
1345
|
assert!(offenses("{foo:}", &c).is_empty());
|
|
1311
1346
|
}
|
|
1347
|
+
|
|
1348
|
+
// 1.91 `hash_rockets_enforced?`: when the whole hash converts to rockets
|
|
1349
|
+
// (`EnforcedStyle: hash_rockets` or `UseHashRocketsWithSymbolValues`),
|
|
1350
|
+
// its pairs skip the shorthand checks instead of also being told to omit
|
|
1351
|
+
// the value (which clobbered the rocket correction).
|
|
1352
|
+
#[test]
|
|
1353
|
+
fn rockets_enforced_hash_skips_shorthand_checks() {
|
|
1354
|
+
// hash_rockets + always: only the rocket offense.
|
|
1355
|
+
assert_eq!(offenses("f(a: a)", &cfg(1, 0)), vec![(2, 4, 1)]);
|
|
1356
|
+
// ruby19 + always + UseHashRocketsWithSymbolValues.
|
|
1357
|
+
let mut c = cfg(0, 0);
|
|
1358
|
+
c.use_hash_rockets_with_symbol_values = true;
|
|
1359
|
+
assert_eq!(offenses("f(a: :sym, b: b)", &c), vec![(2, 4, 1), (11, 13, 1)]);
|
|
1360
|
+
// hash_rockets + consistent: no mixed-shorthand offense either.
|
|
1361
|
+
assert!(offenses("f(a: a, b: 1)", &cfg(1, 3)).iter().all(|o| o.2 == 1));
|
|
1362
|
+
// Without rockets the shorthand check still fires.
|
|
1363
|
+
assert_eq!(offenses("f(a: a)", &cfg(0, 0)).len(), 1);
|
|
1364
|
+
assert_eq!(offenses("f(a: a)", &cfg(0, 0))[0].2, 3);
|
|
1365
|
+
}
|
|
1312
1366
|
}
|
|
@@ -334,7 +334,11 @@ impl<'a> Visitor<'a> {
|
|
|
334
334
|
} else {
|
|
335
335
|
(begin_pos as isize - indentation) as usize
|
|
336
336
|
};
|
|
337
|
-
|
|
337
|
+
// `pos = ind <= begin_pos ? ind..begin_pos : begin_pos..ind` (1.91):
|
|
338
|
+
// ordered by position, not by the sign of the delta, so an
|
|
339
|
+
// under-indented tab body (negative delta but `ind` behind
|
|
340
|
+
// `begin_pos`) still yields a forward range.
|
|
341
|
+
if ind <= begin_pos {
|
|
338
342
|
(ind, begin_pos)
|
|
339
343
|
} else {
|
|
340
344
|
(begin_pos, ind)
|
|
@@ -1307,4 +1311,15 @@ mod tests {
|
|
|
1307
1311
|
});
|
|
1308
1312
|
assert!(got.is_empty());
|
|
1309
1313
|
}
|
|
1314
|
+
|
|
1315
|
+
// 1.91: an under-indented tab body yields a forward offense range
|
|
1316
|
+
// (`ind <= begin_pos ? ind..begin_pos : begin_pos..ind`), with the
|
|
1317
|
+
// negative tab count in the message.
|
|
1318
|
+
#[test]
|
|
1319
|
+
fn tabs_under_indented_body_has_forward_range() {
|
|
1320
|
+
let got = run_cfg("\t\ta = if b\n\tc\n\t\tend\n", |c| c.use_tabs = true);
|
|
1321
|
+
assert_eq!(got.len(), 1);
|
|
1322
|
+
assert!(got[0].message.contains("Use 1 (not -1) tabs"), "{}", got[0].message);
|
|
1323
|
+
assert!(got[0].start_offset <= got[0].end_offset);
|
|
1324
|
+
}
|
|
1310
1325
|
}
|
|
@@ -373,6 +373,17 @@ fn offset_after_chars(source: &[u8], begin: usize, n_chars: usize) -> usize {
|
|
|
373
373
|
|
|
374
374
|
/// `rindex(/\\(u[\da-f]{0,4}|x[\da-f]{0,2})?\z/)` — the char index of a trailing
|
|
375
375
|
/// escape sequence (the backslash), if the substring ends in one.
|
|
376
|
+
/// `rindex(/\s/)` — the char index of the last whitespace in `substr`.
|
|
377
|
+
fn rindex_whitespace(substr: &str) -> Option<usize> {
|
|
378
|
+
substr.char_indices().rev().find_map(|(i, c)| {
|
|
379
|
+
if c.is_whitespace() {
|
|
380
|
+
Some(substr[..i].chars().count())
|
|
381
|
+
} else {
|
|
382
|
+
None
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
376
387
|
fn trailing_escape_char_index(substr: &str) -> Option<usize> {
|
|
377
388
|
let chars: Vec<char> = substr.chars().collect();
|
|
378
389
|
// Find the rightmost backslash whose suffix (to end) matches the pattern.
|
|
@@ -881,14 +892,11 @@ impl<'pr> BreakableVisitor<'_> {
|
|
|
881
892
|
range_end: usize,
|
|
882
893
|
) -> Option<usize> {
|
|
883
894
|
let substr = self.largest_possible_string(node, parent_quote, range_begin, range_end);
|
|
884
|
-
// rindex(/\s/)
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
None
|
|
890
|
-
}
|
|
891
|
-
}) {
|
|
895
|
+
// `breakable_space_position` (1.91): `rindex(/\s/)`, but a space that
|
|
896
|
+
// is the last character of the string content is no break point (the
|
|
897
|
+
// split would leave the content unchanged and loop); retry on the
|
|
898
|
+
// substring before it.
|
|
899
|
+
if let Some(space_char_idx) = self.breakable_space_position(node, range_begin, &substr) {
|
|
892
900
|
// resize(space_pos + 1)
|
|
893
901
|
return Some(offset_after_chars(
|
|
894
902
|
self.source,
|
|
@@ -918,6 +926,40 @@ impl<'pr> BreakableVisitor<'_> {
|
|
|
918
926
|
Some(offset_after_chars(self.source, range_begin, new_chars))
|
|
919
927
|
}
|
|
920
928
|
|
|
929
|
+
/// `breakable_space_position(node, substr)` — the char index of the last
|
|
930
|
+
/// whitespace in `substr`, unless that space sits at the end of the string
|
|
931
|
+
/// content (`space_pos + 1 > string_content_length - 1`); then the last
|
|
932
|
+
/// whitespace of `substr[0, limit]`.
|
|
933
|
+
fn breakable_space_position(
|
|
934
|
+
&self,
|
|
935
|
+
node: &Node<'pr>,
|
|
936
|
+
range_begin: usize,
|
|
937
|
+
substr: &str,
|
|
938
|
+
) -> Option<usize> {
|
|
939
|
+
let limit = self.string_content_length(node, range_begin) as isize - 1;
|
|
940
|
+
let space_pos = rindex_whitespace(substr)?;
|
|
941
|
+
if space_pos as isize + 1 > limit {
|
|
942
|
+
if limit <= 0 {
|
|
943
|
+
return None;
|
|
944
|
+
}
|
|
945
|
+
let head: String = substr.chars().take(limit as usize).collect();
|
|
946
|
+
return rindex_whitespace(&head);
|
|
947
|
+
}
|
|
948
|
+
Some(space_pos)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/// `string_content_length(node)`: chars from the node's start to its
|
|
952
|
+
/// closing delimiter (`loc.end`), or to the node's end when it has none
|
|
953
|
+
/// (a dstr part).
|
|
954
|
+
fn string_content_length(&self, node: &Node<'pr>, range_begin: usize) -> usize {
|
|
955
|
+
let content_end = node
|
|
956
|
+
.as_string_node()
|
|
957
|
+
.and_then(|s| s.closing_loc())
|
|
958
|
+
.map(|c| c.start_offset())
|
|
959
|
+
.unwrap_or_else(|| node.location().end_offset());
|
|
960
|
+
char_count(&self.source[range_begin..content_end])
|
|
961
|
+
}
|
|
962
|
+
|
|
921
963
|
/// `largest_possible_string` — the leading substring (as a `&str`) considered
|
|
922
964
|
/// when choosing a break point.
|
|
923
965
|
fn largest_possible_string(
|
|
@@ -1742,4 +1784,17 @@ mod tests {
|
|
|
1742
1784
|
// Broken as a hash (no string delimiter).
|
|
1743
1785
|
assert_eq!(r[0].2, "");
|
|
1744
1786
|
}
|
|
1787
|
+
|
|
1788
|
+
// 1.91 `breakable_space_position`: a space that is the last character of
|
|
1789
|
+
// the string content is no break point (the split would change nothing
|
|
1790
|
+
// and loop); the previous space is used instead.
|
|
1791
|
+
#[test]
|
|
1792
|
+
fn split_string_skips_a_trailing_space_break_point() {
|
|
1793
|
+
let src = "foo(\"aaaaaaaaaaaaaaaaaaaaaaaaa#{b} cc dd \" \\\n \"ee\")\n";
|
|
1794
|
+
let got = run(src, 40, true);
|
|
1795
|
+
assert_eq!(got.len(), 1);
|
|
1796
|
+
assert_eq!(got[0].0, 0);
|
|
1797
|
+
assert_eq!(got[0].1, src.find("dd \"").unwrap());
|
|
1798
|
+
assert_eq!(got[0].2, "\"");
|
|
1799
|
+
}
|
|
1745
1800
|
}
|
|
@@ -208,4 +208,13 @@ mod tests {
|
|
|
208
208
|
Some((3, 2))
|
|
209
209
|
);
|
|
210
210
|
}
|
|
211
|
+
|
|
212
|
+
// 1.91: `warn_indent` is a magic comment (simple and Emacs forms).
|
|
213
|
+
#[test]
|
|
214
|
+
fn warn_indent_then_encoding() {
|
|
215
|
+
assert_eq!(run("# warn_indent: true\n# encoding: ascii\n"), Some((2, 1)));
|
|
216
|
+
assert_eq!(run("# warn-indent: true\n# encoding: ascii\n"), Some((2, 1)));
|
|
217
|
+
assert_eq!(run("# -*- warn_indent: true -*-\n# encoding: ascii\n"), Some((2, 1)));
|
|
218
|
+
assert_eq!(run("# encoding: ascii\n# warn_indent: true\n"), None);
|
|
219
|
+
}
|
|
211
220
|
}
|
|
@@ -113,6 +113,14 @@ impl ParenthesesAsGroupedExpressionVisitor {
|
|
|
113
113
|
let first_arg = &args[0];
|
|
114
114
|
let Some(parens) = first_arg.as_parentheses_node() else { return; };
|
|
115
115
|
|
|
116
|
+
// `invalid_bare_argument?` (1.91): the parentheses are required when
|
|
117
|
+
// their (possibly re-parenthesised) sole expression could not stand as
|
|
118
|
+
// a bare argument — `and` / `or` / `not`, a modifier `rescue`, or a
|
|
119
|
+
// modifier `if` / `unless` / `while` / `until`.
|
|
120
|
+
if invalid_bare_argument(first_arg) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
// `node.operator_method?` / `node.setter_method?` — outer method name.
|
|
117
125
|
// The `ParenthesesNode` gate already rejects block / chain / hash /
|
|
118
126
|
// ternary / unparenthesised range first arguments by construction
|
|
@@ -149,6 +157,78 @@ impl ParenthesesAsGroupedExpressionVisitor {
|
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
|
|
160
|
+
/// Stock's `invalid_bare_argument?`:
|
|
161
|
+
///
|
|
162
|
+
/// ```ruby
|
|
163
|
+
/// node = node.children.first while node&.begin_type? && node.children.one?
|
|
164
|
+
/// return false unless node
|
|
165
|
+
/// keyword_operator?(node) || modifier_expression?(node)
|
|
166
|
+
/// ```
|
|
167
|
+
///
|
|
168
|
+
/// Parser wraps `(...)` in a `begin` whose children are the statements; prism
|
|
169
|
+
/// gives a `ParenthesesNode` whose `body` is a `StatementsNode`. Peeling a
|
|
170
|
+
/// single-statement parenthesis repeatedly lands on the innermost expression
|
|
171
|
+
/// (or on nothing for `()`).
|
|
172
|
+
fn invalid_bare_argument(node: &Node<'_>) -> bool {
|
|
173
|
+
if let Some(parens) = node.as_parentheses_node() {
|
|
174
|
+
// `()`: `children.first` is nil.
|
|
175
|
+
let Some(body) = parens.body() else { return false };
|
|
176
|
+
if let Some(stmts) = body.as_statements_node() {
|
|
177
|
+
let children: Vec<Node<'_>> = stmts.body().iter().collect();
|
|
178
|
+
if children.len() == 1 {
|
|
179
|
+
return invalid_bare_argument(&children[0]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// A multi-statement `begin` is neither a keyword operator nor a
|
|
183
|
+
// modifier expression.
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
keyword_operator(node) || modifier_expression(node)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// `keyword_operator?`: `(operator_keyword? && semantic_operator?)` — an
|
|
190
|
+
/// `and` / `or` spelled with the keyword — or a modifier `rescue`, or a `not`
|
|
191
|
+
/// call (`send_type? && prefix_not?`).
|
|
192
|
+
fn keyword_operator(node: &Node<'_>) -> bool {
|
|
193
|
+
if let Some(n) = node.as_and_node() {
|
|
194
|
+
return n.operator_loc().as_slice() == b"and";
|
|
195
|
+
}
|
|
196
|
+
if let Some(n) = node.as_or_node() {
|
|
197
|
+
return n.operator_loc().as_slice() == b"or";
|
|
198
|
+
}
|
|
199
|
+
if node.as_rescue_modifier_node().is_some() {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if let Some(c) = node.as_call_node() {
|
|
203
|
+
// `prefix_not?` = `keyword_not?`: the selector is spelled `not`.
|
|
204
|
+
return c.message_loc().is_some_and(|m| m.as_slice() == b"not");
|
|
205
|
+
}
|
|
206
|
+
false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// `modifier_expression?`: `type?(:if, :while, :until) && modifier_form?`. In
|
|
210
|
+
/// prism a modifier form has its keyword after the node's start (the body
|
|
211
|
+
/// comes first). `begin ... end while x` is parser's `while_post`, which
|
|
212
|
+
/// `type?(:while)` does not match, so the begin-modifier flag excludes it.
|
|
213
|
+
fn modifier_expression(node: &Node<'_>) -> bool {
|
|
214
|
+
let node_start = node.location().start_offset();
|
|
215
|
+
if let Some(n) = node.as_if_node() {
|
|
216
|
+
return n
|
|
217
|
+
.if_keyword_loc()
|
|
218
|
+
.is_some_and(|k| k.start_offset() > node_start);
|
|
219
|
+
}
|
|
220
|
+
if let Some(n) = node.as_unless_node() {
|
|
221
|
+
return n.keyword_loc().start_offset() > node_start;
|
|
222
|
+
}
|
|
223
|
+
if let Some(n) = node.as_while_node() {
|
|
224
|
+
return !n.is_begin_modifier() && n.keyword_loc().start_offset() > node_start;
|
|
225
|
+
}
|
|
226
|
+
if let Some(n) = node.as_until_node() {
|
|
227
|
+
return !n.is_begin_modifier() && n.keyword_loc().start_offset() > node_start;
|
|
228
|
+
}
|
|
229
|
+
false
|
|
230
|
+
}
|
|
231
|
+
|
|
152
232
|
/// rubocop-ast `OPERATOR_METHODS` — every operator that can be defined as a
|
|
153
233
|
/// method. Identical to the table in `nested_parenthesized_calls`. The list is
|
|
154
234
|
/// closed (no Ruby version adds new ones in practice), so we encode it
|
|
@@ -407,4 +487,21 @@ mod tests {
|
|
|
407
487
|
// selector `func` ends at 6, arg `(x)` starts at 9.
|
|
408
488
|
assert_eq!(off, vec![(6, 9, 9, 12)]);
|
|
409
489
|
}
|
|
490
|
+
|
|
491
|
+
// 1.91 `invalid_bare_argument?`: parentheses around an `and` / `or` /
|
|
492
|
+
// `not`, a modifier `rescue` or a modifier conditional are required, so
|
|
493
|
+
// they are not a grouped expression; `&&` still is.
|
|
494
|
+
#[test]
|
|
495
|
+
fn accepts_parentheses_required_by_keyword_operators_and_modifiers() {
|
|
496
|
+
assert!(detect("false? (foo and bar)\n").is_empty());
|
|
497
|
+
assert!(detect("false? (foo or bar)\n").is_empty());
|
|
498
|
+
assert!(detect("false? (not foo)\n").is_empty());
|
|
499
|
+
assert!(detect("false? (foo rescue bar)\n").is_empty());
|
|
500
|
+
assert!(detect("false? (foo if bar)\n").is_empty());
|
|
501
|
+
assert!(detect("false? (foo unless bar)\n").is_empty());
|
|
502
|
+
assert!(detect("false? (foo while bar)\n").is_empty());
|
|
503
|
+
assert!(detect("false? ((foo and bar))\n").is_empty());
|
|
504
|
+
assert_eq!(detect("false? (foo && bar)\n"), vec![(6, 7, 7, 19)]);
|
|
505
|
+
assert_eq!(detect("false? (!foo)\n"), vec![(6, 7, 7, 13)]);
|
|
506
|
+
}
|
|
410
507
|
}
|
|
@@ -108,8 +108,10 @@ pub struct SpaceAroundOperatorsOffense {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/// Whether an operator routes through the assignment alignment path (`:assignment`)
|
|
111
|
-
/// or the generic operator alignment path (everything else). Stock
|
|
112
|
-
///
|
|
111
|
+
/// or the generic operator alignment path (everything else). Stock's
|
|
112
|
+
/// `excess_leading_space?` distinguishes `:assignment`, `:special_asgn` (which
|
|
113
|
+
/// joins the assignment path only under `ForceEqualSignAlignment`, 1.91) and
|
|
114
|
+
/// the rest.
|
|
113
115
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
114
116
|
enum AlignType {
|
|
115
117
|
/// `on_assignment` for a plain assignment (`lvasgn` / `casgn` / ... /
|
|
@@ -117,8 +119,9 @@ enum AlignType {
|
|
|
117
119
|
/// diverts `op_asgn_type?` — the arithmetic `+=` family — to
|
|
118
120
|
/// `:special_asgn`.
|
|
119
121
|
Assignment,
|
|
120
|
-
///
|
|
121
|
-
|
|
122
|
+
/// `:special_asgn`: the `+=`-style op-assign family and setter methods.
|
|
123
|
+
SpecialAsgn,
|
|
124
|
+
/// Everything else (binary, ternary, pair, class, ...).
|
|
122
125
|
Other,
|
|
123
126
|
}
|
|
124
127
|
|
|
@@ -501,7 +504,11 @@ fn excess_leading_space(
|
|
|
501
504
|
if !config.allow_for_alignment {
|
|
502
505
|
return false;
|
|
503
506
|
}
|
|
504
|
-
|
|
507
|
+
// `grouped_alignment_type?(type)` (1.91): `:assignment`, or `:special_asgn`
|
|
508
|
+
// when `Layout/ExtraSpacing` forces equal-sign alignment.
|
|
509
|
+
let grouped = align_type == AlignType::Assignment
|
|
510
|
+
|| (align_type == AlignType::SpecialAsgn && config.force_equal_sign_alignment);
|
|
511
|
+
if !grouped {
|
|
505
512
|
// `!aligned_with_operator?(operator)`.
|
|
506
513
|
return !aligner.aligned_with_operator(op_start, op_end);
|
|
507
514
|
}
|
|
@@ -777,7 +784,7 @@ impl Visitor<'_> {
|
|
|
777
784
|
if let Some(arg) = self.first_argument(node) {
|
|
778
785
|
let (rs, re) = (arg.start_offset(), arg.end_offset());
|
|
779
786
|
self.check_operator(
|
|
780
|
-
AlignType::
|
|
787
|
+
AlignType::SpecialAsgn,
|
|
781
788
|
eq.start_offset(),
|
|
782
789
|
eq.end_offset(),
|
|
783
790
|
rs,
|
|
@@ -1017,10 +1024,10 @@ impl Visitor<'_> {
|
|
|
1017
1024
|
);
|
|
1018
1025
|
}
|
|
1019
1026
|
|
|
1020
|
-
/// `on_assignment` for an op-assign (`op_asgn_type?`): :special_asgn
|
|
1027
|
+
/// `on_assignment` for an op-assign (`op_asgn_type?`): :special_asgn.
|
|
1021
1028
|
fn check_op_assign<'pr>(&mut self, op: Location<'pr>, right: Location<'pr>) {
|
|
1022
1029
|
self.check_operator(
|
|
1023
|
-
AlignType::
|
|
1030
|
+
AlignType::SpecialAsgn,
|
|
1024
1031
|
op.start_offset(),
|
|
1025
1032
|
op.end_offset(),
|
|
1026
1033
|
right.start_offset(),
|
|
@@ -1266,4 +1273,19 @@ end
|
|
|
1266
1273
|
assert!(off.iter().any(|o| o.2 == MessageKind::Missing
|
|
1267
1274
|
&& &"uri.path+= '/'\n".as_bytes()[o.0..o.1] == b"+="));
|
|
1268
1275
|
}
|
|
1276
|
+
|
|
1277
|
+
/// 1.91 `grouped_alignment_type?`: under `ForceEqualSignAlignment` an
|
|
1278
|
+
/// op-assign (`:special_asgn`) takes the assignment alignment path, so a
|
|
1279
|
+
/// `+=` aligned with a preceding `=` is accepted; without it the generic
|
|
1280
|
+
/// operator rule still flags the padding.
|
|
1281
|
+
#[test]
|
|
1282
|
+
fn op_assign_aligned_with_preceding_assignment_under_force_alignment() {
|
|
1283
|
+
let src = "aaaa = 1\nfoo\nb += 2\n";
|
|
1284
|
+
let forced = Config {
|
|
1285
|
+
force_equal_sign_alignment: true,
|
|
1286
|
+
..default_cfg()
|
|
1287
|
+
};
|
|
1288
|
+
assert!(run(src, forced).is_empty());
|
|
1289
|
+
assert_eq!(run(src, default_cfg()).len(), 1);
|
|
1290
|
+
}
|
|
1269
1291
|
}
|
data/ext/shirobai/src/lib.rs
CHANGED
|
@@ -2658,21 +2658,22 @@ fn check_line_end_concatenation(source: RString) -> Vec<(usize, usize, String, u
|
|
|
2658
2658
|
|
|
2659
2659
|
/// Ruby entry point for `Layout/ArgumentAlignment`. Takes the source, the
|
|
2660
2660
|
/// enforced style (0=with_first_argument, 1=with_fixed_indentation), the
|
|
2661
|
-
/// configured indentation width and whether
|
|
2662
|
-
///
|
|
2663
|
-
///
|
|
2661
|
+
/// configured indentation width and whether `Layout/HashAlignment` aligns on
|
|
2662
|
+
/// separators (`enforce_hash_argument_with_separator?`; with
|
|
2663
|
+
/// `with_first_argument` that disables the cop). Returns
|
|
2664
|
+
/// `[[start, end, column_delta, autocorrect], ...]`.
|
|
2664
2665
|
fn check_argument_alignment(
|
|
2665
2666
|
source: RString,
|
|
2666
2667
|
style: u8,
|
|
2667
2668
|
indent_width: usize,
|
|
2668
|
-
|
|
2669
|
+
hash_separator: bool,
|
|
2669
2670
|
) -> Vec<(usize, usize, isize, bool)> {
|
|
2670
2671
|
map_argument_alignment(
|
|
2671
2672
|
shirobai_core::rules::argument_alignment::check_argument_alignment(
|
|
2672
2673
|
bytes(&source),
|
|
2673
2674
|
style,
|
|
2674
2675
|
indent_width,
|
|
2675
|
-
|
|
2676
|
+
hash_separator,
|
|
2676
2677
|
),
|
|
2677
2678
|
)
|
|
2678
2679
|
}
|
|
@@ -27,20 +27,23 @@ module Shirobai
|
|
|
27
27
|
def self.badge = RuboCop::Cop::Badge.parse("Layout/ArgumentAlignment")
|
|
28
28
|
|
|
29
29
|
# Packed args for the bundled run: `[style, indentation_width,
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
30
|
+
# hash_separator]`. `hash_separator` is stock's
|
|
31
|
+
# `enforce_hash_argument_with_separator?` (a separator-aligned
|
|
32
|
+
# `Layout/HashAlignment`). Rust derives the rest from it: with
|
|
33
|
+
# `with_first_argument` it disables the cop
|
|
34
|
+
# (`autocorrect_incompatible_with_other_cops?`); with
|
|
35
|
+
# `with_fixed_indentation` a trailing braceless hash's pairs are left
|
|
36
|
+
# to HashAlignment (1.91).
|
|
33
37
|
def self.bundle_args(config)
|
|
34
38
|
cop_config = config.for_badge(badge)
|
|
35
39
|
enforced_style = cop_config["EnforcedStyle"]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
40
|
+
hash_separator = RuboCop::Cop::Layout::HashAlignment::SEPARATOR_ALIGNMENT_STYLES.any? do |sep_style|
|
|
41
|
+
config.for_enabled_cop("Layout/HashAlignment")[sep_style]&.include?("separator")
|
|
42
|
+
end
|
|
40
43
|
[
|
|
41
44
|
enforced_style == "with_fixed_indentation" ? 1 : 0,
|
|
42
45
|
cop_config["IndentationWidth"] || config.for_cop("Layout/IndentationWidth")["Width"] || 2,
|
|
43
|
-
|
|
46
|
+
hash_separator
|
|
44
47
|
]
|
|
45
48
|
end
|
|
46
49
|
|
|
@@ -91,8 +91,10 @@ module Shirobai
|
|
|
91
91
|
if has_value
|
|
92
92
|
correct_key_value(corrector, buffer, off, deltas, key, op, value)
|
|
93
93
|
else
|
|
94
|
-
# `correct_no_value`: adjust the whole node by the key delta
|
|
95
|
-
|
|
94
|
+
# `correct_no_value`: adjust the whole node by the key delta,
|
|
95
|
+
# clamped so the node is not shifted past the start of its
|
|
96
|
+
# line (`clamped_key_delta`, 1.91).
|
|
97
|
+
adjust(corrector, clamped_key_delta(deltas[0], range.column), range)
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
rescue Parser::ClobberingError
|
|
@@ -109,17 +111,21 @@ module Shirobai
|
|
|
109
111
|
key_delta, separator_delta, value_delta = deltas
|
|
110
112
|
key_start, _key_end, key_column = key
|
|
111
113
|
|
|
112
|
-
key_delta = -key_column if key_delta < -key_column
|
|
113
|
-
|
|
114
114
|
key_range = Parser::Source::Range.new(buffer, off[key_start], off[key[1]])
|
|
115
115
|
op_range = Parser::Source::Range.new(buffer, off[op[0]], off[op[1]])
|
|
116
116
|
value_range = Parser::Source::Range.new(buffer, off[value[0]], off[value[1]])
|
|
117
117
|
|
|
118
|
-
adjust(corrector, key_delta, key_range)
|
|
118
|
+
adjust(corrector, clamped_key_delta(key_delta, key_column), key_range)
|
|
119
119
|
adjust(corrector, separator_delta, op_range)
|
|
120
120
|
adjust(corrector, value_delta, value_range)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# Stock's `clamped_key_delta`: never remove more characters than the
|
|
124
|
+
# key has before it on its line.
|
|
125
|
+
def clamped_key_delta(key_delta, key_column)
|
|
126
|
+
[key_delta, -key_column].max
|
|
127
|
+
end
|
|
128
|
+
|
|
123
129
|
# Mirrors stock's `adjust`: insert spaces for a positive delta, remove
|
|
124
130
|
# `delta.abs` characters before the range for a negative one.
|
|
125
131
|
def adjust(corrector, delta, range)
|
data/lib/shirobai/version.rb
CHANGED
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.
|
|
4
|
+
version: 2026.0920.1100
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fusagiko / takayamaki
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 1.
|
|
32
|
+
version: 1.91.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 1.
|
|
39
|
+
version: 1.91.0
|
|
40
40
|
executables: []
|
|
41
41
|
extensions:
|
|
42
42
|
- ext/shirobai/extconf.rb
|