rfmt 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 327f57f105df77a0d6b77bda9ab87c4f234f03f23fb918d74679942456c5a8ec
4
- data.tar.gz: 482a5406275e422fb306970ffb47708f37be0c3115bc92485996d2adde6c9463
3
+ metadata.gz: 4fd5d79157230ea6ee461e070a3782267196b57692c37fc0e5e06a33f2dbd6fd
4
+ data.tar.gz: ea5b633304e771cd80a235ae79676581791f5f1f041de4d4013fd22e60b3cd5c
5
5
  SHA512:
6
- metadata.gz: 52982745650ed439f2ac7a5a1a9a26ae2dcf9e9df8015526278f58508929e15a1d72425d2d3c5963c0a4008968e20fcb6c374881122a3e99618d2cef15434eaf
7
- data.tar.gz: 10e8282f05caacdffcbb2d7042e9f95a08c5d08c9b081307a12752840549fbb08b6bc557a256a9a46fd012986369fd7cfd0442a46d1d81146c2fb234d0b936f7
6
+ metadata.gz: 1d81952b2c5b9b9a3ba04617c60c1742d6141390be0da810bdc71ab2661bd767008bf7b744b1337496035bfeab39ee9db672163f23e9d693aad681edc8236256
7
+ data.tar.gz: '0950ad8b8a68dc96bb20fd980751805704b6f1a6be07268210fe309ba4b6642469899f61c62d8157770956732c272636d072d9ad29d148ff555335897df2ae0e'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.6.3] - 2026-04-24
4
+
5
+ Minor stability refinements on top of the 1.6.x architecture release. See the 1.6.1 notes below for the feature set this series delivers.
6
+
7
+ ## [1.6.2] - 2026-04-24
8
+
9
+ Minor stability refinements on top of the 1.6.x architecture release. See the 1.6.1 notes below for the feature set this series delivers.
10
+
3
11
  ## [1.6.1] - 2026-04-24
4
12
 
5
13
  Follow-up release consolidating the 1.6.0 architecture work.
data/Cargo.lock CHANGED
@@ -1251,7 +1251,7 @@ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
1251
1251
 
1252
1252
  [[package]]
1253
1253
  name = "rfmt"
1254
- version = "1.6.1"
1254
+ version = "1.6.3"
1255
1255
  dependencies = [
1256
1256
  "anyhow",
1257
1257
  "clap",
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "1.6.1"
3
+ version = "1.6.3"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -217,6 +217,23 @@ pub fn format_comments_before_end(
217
217
  }
218
218
 
219
219
  let mut docs: Vec<Doc> = vec![hardline()];
220
+ // Preserve the blank line that separated the body's last content from
221
+ // the first trailing comment. Without this, a construct like
222
+ //
223
+ // def foo
224
+ // body
225
+ // <- blank line
226
+ // # trailing annotation
227
+ // end
228
+ //
229
+ // collapses to `body\n# trailing annotation\nend`. Detect the case
230
+ // heuristically: if the source line immediately above the first
231
+ // standalone comment is blank, emit an extra hardline.
232
+ if let Some(first) = standalone_refs.first() {
233
+ if first.start_line > 1 && is_line_blank(ctx.source(), first.start_line - 1) {
234
+ docs.push(hardline());
235
+ }
236
+ }
220
237
  let mut last_end_line: Option<usize> = None;
221
238
  let mut indices_to_mark: Vec<usize> = Vec::with_capacity(standalone_refs.len());
222
239
 
@@ -297,11 +314,23 @@ pub fn format_remaining_comments(ctx: &mut FormatContext, last_code_line: usize)
297
314
  let mut indices_to_mark: Vec<usize> = Vec::with_capacity(comment_refs.len());
298
315
 
299
316
  for cref in &comment_refs {
300
- // Preserve blank lines
317
+ // Preserve blank lines. On the first iteration we must emit *at
318
+ // least one* hardline to separate the first remaining comment from
319
+ // the main document's last token (otherwise an orphan comment whose
320
+ // `start_line <= last_code_line` would concatenate onto whatever
321
+ // ended the output — producing e.g. `end# comment…` when a block's
322
+ // internal comments fall through to this tail handler). Round-tripping
323
+ // the already-formatted output must still be idempotent, so we
324
+ // cap the emission at the number of line breaks visible in the
325
+ // source: 1 for an adjacent comment, N for N-1 blank lines above it.
301
326
  let gap = cref.start_line.saturating_sub(last_end_line);
302
327
 
303
- // Only add newlines if not the first comment or if there's a gap
304
- if !is_first || gap > 0 {
328
+ if is_first {
329
+ let hardlines_to_emit = gap.max(1);
330
+ for _ in 0..hardlines_to_emit {
331
+ docs.push(hardline());
332
+ }
333
+ } else if gap > 0 {
305
334
  for _ in 0..gap.max(1) {
306
335
  docs.push(hardline());
307
336
  }
@@ -479,8 +508,42 @@ pub fn reformat_chain_lines(
479
508
  return Cow::Borrowed(source_text);
480
509
  }
481
510
 
482
- // Build the indented chain with pre-allocated capacity
483
- let chain_indent = " ".repeat(base_indent + indent_width);
511
+ // Determine how much the chain is moving left. Before this pass, every
512
+ // `.method` line sat at some original "chain indent" (most commonly
513
+ // aligned under the first receiver's dot). We rewrite those lines to
514
+ // `base_indent + indent_width` — but that also means any multi-line
515
+ // *arguments* that lived inside a chain call like `.select( … )` used
516
+ // to be deeper than the original chain indent, and will now look
517
+ // orphaned off to the right if we leave them alone:
518
+ //
519
+ // @users = User.left_joins(...)
520
+ // .select( <- was col 17, becomes col 6
521
+ // 'users.*, ' \ <- still at col 19 — orphaned
522
+ // )
523
+ // .having(...)
524
+ //
525
+ // Compute the delta between the original chain indent and the new
526
+ // one, and shift every non-chain continuation line that lives at
527
+ // (or below) the original chain indent by the same amount. Lines
528
+ // shallower than the original chain indent (e.g. a heredoc body
529
+ // whose squiggly indent is measured from the terminator) are left
530
+ // alone, so we don't accidentally eat through them.
531
+ let new_chain_indent = base_indent + indent_width;
532
+ let chain_indent_str = " ".repeat(new_chain_indent);
533
+
534
+ let original_chain_indent = lines[1..].iter().find_map(|l| {
535
+ let t = l.trim_start();
536
+ if t.starts_with('.') || t.starts_with("&.") {
537
+ Some(l.len() - t.len())
538
+ } else {
539
+ None
540
+ }
541
+ });
542
+
543
+ let arg_shift = original_chain_indent
544
+ .map(|orig| orig.saturating_sub(new_chain_indent))
545
+ .unwrap_or(0);
546
+
484
547
  let mut result = String::with_capacity(source_text.len());
485
548
  result.push_str(lines[0].trim_end());
486
549
 
@@ -488,8 +551,18 @@ pub fn reformat_chain_lines(
488
551
  result.push('\n');
489
552
  let trimmed = line.trim();
490
553
  if trimmed.starts_with('.') || trimmed.starts_with("&.") {
491
- result.push_str(&chain_indent);
554
+ result.push_str(&chain_indent_str);
492
555
  result.push_str(trimmed);
556
+ } else if arg_shift > 0 && !trimmed.is_empty() {
557
+ let indent = line.len() - line.trim_start().len();
558
+ let chain_base = original_chain_indent.unwrap_or(0);
559
+ if indent >= chain_base {
560
+ let new_indent = indent - arg_shift;
561
+ result.push_str(&" ".repeat(new_indent));
562
+ result.push_str(line.trim_start());
563
+ } else {
564
+ result.push_str(line);
565
+ }
493
566
  } else {
494
567
  // Non-chain continuation (e.g., heredoc content): preserve as-is
495
568
  result.push_str(line);
@@ -499,6 +572,27 @@ pub fn reformat_chain_lines(
499
572
  Cow::Owned(result)
500
573
  }
501
574
 
575
+ /// Returns true when the given 1-based `line` in `source` contains only
576
+ /// whitespace (or is empty). Returns false for any line that has code or
577
+ /// a comment.
578
+ fn is_line_blank(source: &str, line: usize) -> bool {
579
+ let mut current = 1usize;
580
+ let mut line_start = 0usize;
581
+ for (i, b) in source.bytes().enumerate() {
582
+ if current == line {
583
+ let end = source[i..].find('\n').map_or(source.len(), |n| i + n);
584
+ return source[line_start..end]
585
+ .bytes()
586
+ .all(|b| b == b' ' || b == b'\t' || b == b'\r');
587
+ }
588
+ if b == b'\n' {
589
+ current += 1;
590
+ line_start = i + 1;
591
+ }
592
+ }
593
+ false
594
+ }
595
+
502
596
  /// Removes at most one trailing `\n` (optionally preceded by a single `\r`)
503
597
  /// from `s`. Spaces, tabs, and any additional preceding newlines are
504
598
  /// preserved.
@@ -13,9 +13,9 @@ use crate::error::Result;
13
13
  use crate::format::context::FormatContext;
14
14
  use crate::format::registry::RuleRegistry;
15
15
  use crate::format::rule::{
16
- format_child, format_leading_comments, format_statements, format_trailing_comment,
17
- line_leading_indent, mark_comments_in_range_emitted, reformat_chain_lines,
18
- strip_one_trailing_newline, FormatRule,
16
+ format_child, format_comments_before_end, format_leading_comments, format_statements,
17
+ format_trailing_comment, line_leading_indent, mark_comments_in_range_emitted,
18
+ reformat_chain_lines, strip_one_trailing_newline, FormatRule,
19
19
  };
20
20
 
21
21
  /// Rule for formatting method calls.
@@ -251,6 +251,23 @@ fn format_do_end_block(
251
251
  }
252
252
  }
253
253
 
254
+ // Emit any standalone comments between the last body statement and `end`.
255
+ //
256
+ // Without this the orphan comments inside a `do…end` block (e.g. the
257
+ // commented-out config stanzas in a generated `spec_helper.rb`) never
258
+ // get claimed by any `format_leading_comments` call, fall through to
259
+ // `format_remaining_comments` at the end of the file, and get emitted
260
+ // *after* the block's own `end` — producing `end# comment…` with no
261
+ // separator and dropping the body indent.
262
+ let comments_before_end = format_comments_before_end(
263
+ ctx,
264
+ block_node.location.start_line,
265
+ block_node.location.end_line,
266
+ );
267
+ if !comments_before_end.is_empty() {
268
+ docs.push(indent(comments_before_end));
269
+ }
270
+
254
271
  // Emit 'end'
255
272
  docs.push(hardline());
256
273
  docs.push(text("end"));
data/lib/rfmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rfmt
4
- VERSION = '1.6.1'
4
+ VERSION = '1.6.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora