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 +4 -4
- data/crates/shirobai-core/src/rules/block_alignment.rs +111 -16
- data/crates/shirobai-core/src/rules/block_delimiters.rs +68 -46
- data/crates/shirobai-core/src/rules/colon_method_call.rs +30 -23
- data/crates/shirobai-core/src/rules/safe_navigation_chain.rs +17 -1
- data/crates/shirobai-core/src/rules/self_assignment.rs +249 -2
- 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: 2da350f27fcd3ff47681028d0508eade78a0ced26a5b96007985da947ab8a292
|
|
4
|
+
data.tar.gz: 680562385119492ea485da4929b883bba60ff15129f6fd9a2e70d230245aaff4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(
|
|
178
|
-
/// from
|
|
179
|
-
///
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
369
|
+
body: Option<&Node<'_>>,
|
|
370
370
|
) -> bool {
|
|
371
|
-
if self.require_do_end(braces, multiline,
|
|
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`
|
|
395
|
-
/// `
|
|
396
|
-
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
///
|
|
1182
|
-
///
|
|
1183
|
-
///
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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 `
|
|
15
|
-
//! `(
|
|
16
|
-
//! `
|
|
17
|
-
//!
|
|
18
|
-
//!
|
|
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
|
-
// `
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
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
|
|
239
|
-
// `Java::foo::bar` —
|
|
240
|
-
|
|
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::
|
|
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
|
-
|
|
18
|
-
|
|
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
|
}
|
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.0620.
|
|
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.
|
|
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.
|
|
25
|
+
version: 1.88.0
|
|
26
26
|
executables: []
|
|
27
27
|
extensions:
|
|
28
28
|
- ext/shirobai/Cargo.toml
|