rfmt 1.7.0 → 2.0.0.beta1
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/CHANGELOG.md +25 -0
- data/Cargo.lock +90 -1225
- data/Cargo.toml +6 -0
- data/README.md +32 -28
- data/ext/rfmt/Cargo.toml +9 -28
- data/ext/rfmt/src/ast/mod.rs +2 -0
- data/ext/rfmt/src/config/mod.rs +267 -30
- data/ext/rfmt/src/error/mod.rs +14 -3
- data/ext/rfmt/src/format/formatter.rs +22 -11
- data/ext/rfmt/src/format/registry.rs +8 -0
- data/ext/rfmt/src/format/rule.rs +208 -136
- data/ext/rfmt/src/format/rules/call.rs +14 -22
- data/ext/rfmt/src/format/rules/fallback.rs +4 -11
- data/ext/rfmt/src/format/rules/if_unless.rs +25 -3
- data/ext/rfmt/src/format/rules/loops.rs +3 -1
- data/ext/rfmt/src/format/rules/variable_write.rs +16 -9
- data/ext/rfmt/src/lib.rs +45 -12
- data/ext/rfmt/src/parser/mod.rs +2 -0
- data/ext/rfmt/src/parser/native_adapter.rs +2735 -0
- data/ext/rfmt/src/parser/prism_adapter.rs +5 -0
- data/ext/rfmt/src/validation.rs +69 -0
- data/ext/rfmt/tests/fixtures/parity/comments_mixed.rb +9 -0
- data/ext/rfmt/tests/fixtures/parity/constructs.rb +50 -0
- data/ext/rfmt/tests/fixtures/parity/embdoc.rb +12 -0
- data/ext/rfmt/tests/fixtures/parity/heredoc_assign.rb +15 -0
- data/ext/rfmt/tests/fixtures/parity/heredoc_call_args.rb +17 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_classes.rb +30 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_conditionals.rb +14 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_defs.rb +33 -0
- data/ext/rfmt/tests/fixtures/parity/multibyte.rb +14 -0
- data/ext/rfmt/tests/fixtures/parity/numeric.rb +6 -0
- data/ext/rfmt/tests/fixtures/parity/plain.rb +31 -0
- data/ext/rfmt/tests/native_parity.rs +245 -0
- data/ext/rfmt/tests/ruby_prism_smoke.rs +66 -0
- data/lib/rfmt/cli.rb +37 -7
- data/lib/rfmt/configuration.rb +2 -20
- data/lib/rfmt/version.rb +1 -1
- data/lib/rfmt.rb +47 -19
- metadata +17 -18
- data/lib/rfmt/prism_bridge.rb +0 -529
- data/lib/rfmt/prism_node_extractor.rb +0 -115
data/ext/rfmt/src/format/rule.rs
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
//! This module defines the core FormatRule trait that all formatting rules implement,
|
|
4
4
|
//! along with shared helper functions for common formatting patterns.
|
|
5
5
|
|
|
6
|
+
use std::collections::VecDeque;
|
|
7
|
+
|
|
6
8
|
use crate::ast::{CommentType, Node};
|
|
7
|
-
use crate::doc::{
|
|
9
|
+
use crate::doc::{
|
|
10
|
+
concat, hardline, indent, leading_comment, literalline, text, trailing_comment, Doc,
|
|
11
|
+
};
|
|
8
12
|
use crate::error::Result;
|
|
9
13
|
|
|
10
14
|
use super::context::FormatContext;
|
|
@@ -437,139 +441,186 @@ pub fn format_statements(
|
|
|
437
441
|
|
|
438
442
|
/// Returns the number of leading space/tab characters on the line containing `offset`.
|
|
439
443
|
///
|
|
440
|
-
///
|
|
441
|
-
///
|
|
442
|
-
///
|
|
443
|
-
/// newlines, so any reformatting that emits a multi-line string must include the
|
|
444
|
-
/// original leading indent itself.
|
|
445
|
-
pub fn line_leading_indent(source: &str, offset: usize) -> usize {
|
|
446
|
-
let offset = offset.min(source.len());
|
|
447
|
-
let line_start = source[..offset].rfind('\n').map(|p| p + 1).unwrap_or(0);
|
|
448
|
-
source.as_bytes()[line_start..offset]
|
|
449
|
-
.iter()
|
|
450
|
-
.take_while(|&&b| b == b' ' || b == b'\t')
|
|
451
|
-
.count()
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/// Reformats multiline method chain text with indented style.
|
|
455
|
-
///
|
|
456
|
-
/// Converts aligned method chains to indented style:
|
|
444
|
+
/// Reformats a multiline method chain into a Doc that lets the printer own
|
|
445
|
+
/// line anchoring, so the output is independent of the statement's column
|
|
446
|
+
/// in the INPUT source (misindented input formats the same as clean input):
|
|
457
447
|
/// - First line is kept as-is (trimmed at end)
|
|
458
|
-
/// -
|
|
459
|
-
/// `
|
|
460
|
-
///
|
|
461
|
-
///
|
|
462
|
-
///
|
|
463
|
-
///
|
|
448
|
+
/// - Chain continuation lines (`.` / `&.`) become `hardline` + trimmed body
|
|
449
|
+
/// inside `indent(...)`, rendering one indent level below wherever the
|
|
450
|
+
/// statement lands in the output
|
|
451
|
+
/// - Other lines at or beyond the original chain indent (multi-line
|
|
452
|
+
/// arguments of a chain call) keep their offset RELATIVE to the chain
|
|
453
|
+
/// lines, baked as leading spaces on top of the hardline anchor
|
|
454
|
+
/// - Heredoc body lines and shallower lines are emitted verbatim after a
|
|
455
|
+
/// `literalline` — for plain/dash heredocs the leading whitespace is
|
|
456
|
+
/// string content, so shifting it would change program semantics
|
|
464
457
|
///
|
|
465
|
-
/// Returns `
|
|
466
|
-
///
|
|
467
|
-
|
|
468
|
-
/// ```text
|
|
469
|
-
/// Input (base_indent=4, indent_width=2):
|
|
470
|
-
/// "foo.bar\n .baz"
|
|
471
|
-
/// Output:
|
|
472
|
-
/// "foo.bar\n .baz"
|
|
473
|
-
/// ```
|
|
474
|
-
pub fn reformat_chain_lines(
|
|
475
|
-
source_text: &str,
|
|
476
|
-
base_indent: usize,
|
|
477
|
-
indent_width: usize,
|
|
478
|
-
) -> std::borrow::Cow<'_, str> {
|
|
479
|
-
use std::borrow::Cow;
|
|
480
|
-
|
|
458
|
+
/// Returns `None` when no transformation applies; callers fall back to the
|
|
459
|
+
/// verbatim source text.
|
|
460
|
+
pub fn reformat_chain_doc(source_text: &str) -> Option<Doc> {
|
|
481
461
|
let lines: Vec<&str> = source_text.lines().collect();
|
|
482
462
|
if lines.len() <= 1 {
|
|
483
|
-
return
|
|
463
|
+
return None;
|
|
484
464
|
}
|
|
485
465
|
|
|
486
466
|
// Skip reformatting when the first line opens a new scope (a `{` brace
|
|
487
467
|
// lambda, a `do` block, or a `do |params|` block). The `.method` lines
|
|
488
468
|
// that follow are chain continuations *inside the block body*, not of
|
|
489
|
-
// the outer call — re-indenting them relative to the outer
|
|
490
|
-
//
|
|
491
|
-
//
|
|
469
|
+
// the outer call — re-indenting them relative to the outer statement
|
|
470
|
+
// collapses the nested chain one level to the left and breaks the
|
|
471
|
+
// visual structure of the block body.
|
|
492
472
|
//
|
|
493
473
|
// This deliberately keeps the reformat conservative: for a top-level
|
|
494
474
|
// `User.active.where(...)` chain, line 1 ends with an identifier so
|
|
495
475
|
// we still rewrite aligned → indented as PR #100 intended.
|
|
496
476
|
let first_line = lines[0].trim_end();
|
|
497
477
|
if first_line.ends_with('{') || first_line.ends_with(" do") || first_line.ends_with('|') {
|
|
498
|
-
return
|
|
478
|
+
return None;
|
|
499
479
|
}
|
|
500
480
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return
|
|
481
|
+
// Cheap pre-check before the heredoc scan; a false positive from a
|
|
482
|
+
// `.`-leading heredoc body line only costs the scan below, whose masked
|
|
483
|
+
// chain_indent search then bails.
|
|
484
|
+
if !lines[1..]
|
|
485
|
+
.iter()
|
|
486
|
+
.any(|l| is_chain_continuation(l.trim_start()))
|
|
487
|
+
{
|
|
488
|
+
return None;
|
|
509
489
|
}
|
|
510
490
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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| {
|
|
491
|
+
let heredoc_body = heredoc_body_lines(&lines);
|
|
492
|
+
|
|
493
|
+
// The original chain indent anchors the relative offsets of multi-line
|
|
494
|
+
// argument lines; without any chain continuation line there is nothing
|
|
495
|
+
// to reformat.
|
|
496
|
+
let chain_indent = lines.iter().enumerate().skip(1).find_map(|(i, l)| {
|
|
497
|
+
if heredoc_body[i] {
|
|
498
|
+
return None;
|
|
499
|
+
}
|
|
535
500
|
let t = l.trim_start();
|
|
536
|
-
if
|
|
501
|
+
if is_chain_continuation(t) {
|
|
537
502
|
Some(l.len() - t.len())
|
|
538
503
|
} else {
|
|
539
504
|
None
|
|
540
505
|
}
|
|
541
|
-
})
|
|
542
|
-
|
|
543
|
-
let
|
|
544
|
-
|
|
545
|
-
.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
506
|
+
})?;
|
|
507
|
+
|
|
508
|
+
let mut rest: Vec<Doc> = Vec::with_capacity((lines.len() - 1) * 2);
|
|
509
|
+
for (i, line) in lines.iter().enumerate().skip(1) {
|
|
510
|
+
let body = line.trim_start();
|
|
511
|
+
let indent_cols = line.len() - body.len();
|
|
512
|
+
if heredoc_body[i] {
|
|
513
|
+
rest.push(literalline());
|
|
514
|
+
rest.push(text((*line).to_string()));
|
|
515
|
+
} else if is_chain_continuation(body) {
|
|
516
|
+
rest.push(hardline());
|
|
517
|
+
rest.push(text(body.trim_end().to_string()));
|
|
518
|
+
} else if !body.is_empty() && indent_cols >= chain_indent {
|
|
519
|
+
// Multi-line arguments inside a chain call like `.select( … )`
|
|
520
|
+
// move together with the chain lines they belong to.
|
|
521
|
+
rest.push(hardline());
|
|
522
|
+
rest.push(text(format!(
|
|
523
|
+
"{}{}",
|
|
524
|
+
" ".repeat(indent_cols - chain_indent),
|
|
525
|
+
body.trim_end()
|
|
526
|
+
)));
|
|
527
|
+
} else {
|
|
528
|
+
rest.push(literalline());
|
|
529
|
+
rest.push(text((*line).to_string()));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
Some(concat(vec![
|
|
534
|
+
text(first_line.to_string()),
|
|
535
|
+
indent(concat(rest)),
|
|
536
|
+
]))
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/// `reformat_chain_doc` with the shared fallback: verbatim source text,
|
|
540
|
+
/// stripped of one trailing newline.
|
|
541
|
+
pub fn chain_doc_or_verbatim(source_text: &str) -> Doc {
|
|
542
|
+
reformat_chain_doc(source_text)
|
|
543
|
+
.unwrap_or_else(|| text(strip_one_trailing_newline(source_text).to_string()))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fn is_chain_continuation(trimmed: &str) -> bool {
|
|
547
|
+
trimmed.starts_with('.') || trimmed.starts_with("&.")
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
struct HeredocOpener {
|
|
551
|
+
terminator: String,
|
|
552
|
+
/// `<<-` / `<<~` allow the terminator line to be indented.
|
|
553
|
+
indented_terminator: bool,
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/// Lexically marks lines that are heredoc bodies (including terminator
|
|
557
|
+
/// lines). Openers on one line stack in source order, and each body runs
|
|
558
|
+
/// until its terminator. When a terminator is ambiguous (e.g. a stray `\r`)
|
|
559
|
+
/// this errs toward marking more lines as body, which only makes the
|
|
560
|
+
/// reformat emit them verbatim — the semantics-safe direction.
|
|
561
|
+
fn heredoc_body_lines(lines: &[&str]) -> Vec<bool> {
|
|
562
|
+
let mut body = vec![false; lines.len()];
|
|
563
|
+
let mut pending: VecDeque<HeredocOpener> = VecDeque::new();
|
|
564
|
+
for (i, line) in lines.iter().enumerate() {
|
|
565
|
+
if let Some(front) = pending.front() {
|
|
566
|
+
body[i] = true;
|
|
567
|
+
let candidate = if front.indented_terminator {
|
|
568
|
+
line.trim_start()
|
|
563
569
|
} else {
|
|
564
|
-
|
|
570
|
+
line
|
|
571
|
+
};
|
|
572
|
+
if candidate.trim_end_matches('\r') == front.terminator {
|
|
573
|
+
pending.pop_front();
|
|
565
574
|
}
|
|
566
|
-
|
|
567
|
-
// Non-chain continuation (e.g., heredoc content): preserve as-is
|
|
568
|
-
result.push_str(line);
|
|
575
|
+
continue;
|
|
569
576
|
}
|
|
577
|
+
scan_heredoc_openers(line, &mut pending);
|
|
570
578
|
}
|
|
579
|
+
body
|
|
580
|
+
}
|
|
571
581
|
|
|
572
|
-
|
|
582
|
+
/// Finds `<<ID` / `<<-ID` / `<<~ID` openers (identifier optionally wrapped
|
|
583
|
+
/// in `"` / `'` / `` ` ``) in a code line. Purely lexical: openers inside
|
|
584
|
+
/// string literals or comments are false positives, tolerated because they
|
|
585
|
+
/// only cause verbatim emission.
|
|
586
|
+
fn scan_heredoc_openers(line: &str, pending: &mut VecDeque<HeredocOpener>) {
|
|
587
|
+
let bytes = line.as_bytes();
|
|
588
|
+
let mut i = 0;
|
|
589
|
+
while i + 2 < bytes.len() {
|
|
590
|
+
if bytes[i] != b'<' || bytes[i + 1] != b'<' {
|
|
591
|
+
i += 1;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
let mut j = i + 2;
|
|
595
|
+
let indented = matches!(bytes.get(j), Some(b'-') | Some(b'~'));
|
|
596
|
+
if indented {
|
|
597
|
+
j += 1;
|
|
598
|
+
}
|
|
599
|
+
let quote = match bytes.get(j) {
|
|
600
|
+
Some(&q @ (b'"' | b'\'' | b'`')) => {
|
|
601
|
+
j += 1;
|
|
602
|
+
Some(q)
|
|
603
|
+
}
|
|
604
|
+
_ => None,
|
|
605
|
+
};
|
|
606
|
+
let start = j;
|
|
607
|
+
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
|
|
608
|
+
j += 1;
|
|
609
|
+
}
|
|
610
|
+
let valid = j > start
|
|
611
|
+
&& (bytes[start].is_ascii_alphabetic() || bytes[start] == b'_')
|
|
612
|
+
&& quote.is_none_or(|q| bytes.get(j) == Some(&q));
|
|
613
|
+
if !valid {
|
|
614
|
+
// Skip both `<` so `a <<< b` or `x <<= y` cannot rescan.
|
|
615
|
+
i += 2;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
pending.push_back(HeredocOpener {
|
|
619
|
+
terminator: line[start..j].to_string(),
|
|
620
|
+
indented_terminator: indented,
|
|
621
|
+
});
|
|
622
|
+
i = j + usize::from(quote.is_some());
|
|
623
|
+
}
|
|
573
624
|
}
|
|
574
625
|
|
|
575
626
|
/// Returns true when the given 1-based `line` in `source` contains only
|
|
@@ -767,54 +818,75 @@ mod tests {
|
|
|
767
818
|
assert!(!matches!(doc, Doc::Empty));
|
|
768
819
|
}
|
|
769
820
|
|
|
821
|
+
fn print(doc: Doc) -> String {
|
|
822
|
+
let config = Config::default();
|
|
823
|
+
let mut printer = crate::doc::Printer::new(&config);
|
|
824
|
+
printer.print(&doc)
|
|
825
|
+
}
|
|
826
|
+
|
|
770
827
|
#[test]
|
|
771
|
-
fn
|
|
772
|
-
|
|
773
|
-
let result = reformat_chain_lines(input, 0, 2);
|
|
774
|
-
assert_eq!(result, "foo.bar.baz");
|
|
828
|
+
fn test_reformat_chain_doc_single_line() {
|
|
829
|
+
assert!(reformat_chain_doc("foo.bar.baz").is_none());
|
|
775
830
|
}
|
|
776
831
|
|
|
777
832
|
#[test]
|
|
778
|
-
fn
|
|
833
|
+
fn test_reformat_chain_doc_multiline_chain() {
|
|
779
834
|
let input = "foo.bar\n .baz\n .qux";
|
|
780
|
-
let
|
|
781
|
-
assert_eq!(
|
|
835
|
+
let doc = reformat_chain_doc(input).unwrap();
|
|
836
|
+
assert_eq!(print(doc), "foo.bar\n .baz\n .qux\n");
|
|
782
837
|
}
|
|
783
838
|
|
|
784
839
|
#[test]
|
|
785
|
-
fn
|
|
840
|
+
fn test_reformat_chain_doc_safe_navigation() {
|
|
786
841
|
let input = "foo&.bar\n &.baz";
|
|
787
|
-
let
|
|
788
|
-
assert_eq!(
|
|
842
|
+
let doc = reformat_chain_doc(input).unwrap();
|
|
843
|
+
assert_eq!(print(doc), "foo&.bar\n &.baz\n");
|
|
789
844
|
}
|
|
790
845
|
|
|
791
846
|
#[test]
|
|
792
|
-
fn
|
|
793
|
-
|
|
794
|
-
let result = reformat_chain_lines(input, 0, 2);
|
|
795
|
-
assert_eq!(result, input);
|
|
847
|
+
fn test_reformat_chain_doc_no_chain() {
|
|
848
|
+
assert!(reformat_chain_doc("foo(\n arg1,\n arg2\n)").is_none());
|
|
796
849
|
}
|
|
797
850
|
|
|
798
851
|
#[test]
|
|
799
|
-
fn
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
let
|
|
804
|
-
let
|
|
805
|
-
|
|
852
|
+
fn test_reformat_chain_doc_independent_of_input_column() {
|
|
853
|
+
// The doc carries no absolute indent: differently misindented
|
|
854
|
+
// inputs of the same chain print identically.
|
|
855
|
+
let aligned = "foo.bar\n .baz";
|
|
856
|
+
let flush = "foo.bar\n.baz";
|
|
857
|
+
let a = print(reformat_chain_doc(aligned).unwrap());
|
|
858
|
+
let b = print(reformat_chain_doc(flush).unwrap());
|
|
859
|
+
assert_eq!(a, b);
|
|
860
|
+
assert_eq!(a, "foo.bar\n .baz\n");
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
#[test]
|
|
864
|
+
fn test_reformat_chain_doc_argument_lines_stay_relative_to_chain() {
|
|
865
|
+
let input = "u = User.all\n .select(\n :id,\n )\n .to_a";
|
|
866
|
+
let doc = reformat_chain_doc(input).unwrap();
|
|
867
|
+
assert_eq!(
|
|
868
|
+
print(doc),
|
|
869
|
+
"u = User.all\n .select(\n :id,\n )\n .to_a\n"
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
#[test]
|
|
874
|
+
fn test_reformat_chain_doc_keeps_plain_heredoc_body_verbatim() {
|
|
875
|
+
let input = "result = base\n .where(<<SQL)\n a = 1\nSQL\n .order(:id)";
|
|
876
|
+
let doc = reformat_chain_doc(input).unwrap();
|
|
877
|
+
assert_eq!(
|
|
878
|
+
print(doc),
|
|
879
|
+
"result = base\n .where(<<SQL)\n a = 1\nSQL\n .order(:id)\n"
|
|
880
|
+
);
|
|
806
881
|
}
|
|
807
882
|
|
|
808
883
|
#[test]
|
|
809
|
-
fn
|
|
810
|
-
let
|
|
811
|
-
let
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
assert_eq!(line_leading_indent(source, qux), 0);
|
|
817
|
-
// Out-of-range offset is clamped.
|
|
818
|
-
assert_eq!(line_leading_indent(source, usize::MAX), 0);
|
|
884
|
+
fn test_reformat_chain_doc_stacked_heredoc_openers() {
|
|
885
|
+
let input = "r = base\n .merge(<<-A, <<~\"B\")\n one\n A\n two\n B\n .to_a";
|
|
886
|
+
let doc = reformat_chain_doc(input).unwrap();
|
|
887
|
+
assert_eq!(
|
|
888
|
+
print(doc),
|
|
889
|
+
"r = base\n .merge(<<-A, <<~\"B\")\n one\n A\n two\n B\n .to_a\n"
|
|
890
|
+
);
|
|
819
891
|
}
|
|
820
892
|
}
|
|
@@ -5,17 +5,15 @@
|
|
|
5
5
|
//! - Calls with blocks: `foo.bar do ... end` or `foo.bar { ... }`
|
|
6
6
|
//! - Method chains: `foo.bar.baz`
|
|
7
7
|
|
|
8
|
-
use std::borrow::Cow;
|
|
9
|
-
|
|
10
8
|
use crate::ast::{Node, NodeType};
|
|
11
9
|
use crate::doc::{align, concat, hardline, indent, text, Doc};
|
|
12
10
|
use crate::error::Result;
|
|
13
11
|
use crate::format::context::FormatContext;
|
|
14
12
|
use crate::format::registry::RuleRegistry;
|
|
15
13
|
use crate::format::rule::{
|
|
16
|
-
format_child, format_comments_before_end, format_leading_comments,
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
chain_doc_or_verbatim, format_child, format_comments_before_end, format_leading_comments,
|
|
15
|
+
format_statements, format_trailing_comment, mark_comments_in_range_emitted, reformat_chain_doc,
|
|
16
|
+
FormatRule,
|
|
19
17
|
};
|
|
20
18
|
|
|
21
19
|
/// Rule for formatting method calls.
|
|
@@ -114,14 +112,7 @@ fn format_call(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) ->
|
|
|
114
112
|
// the full `trim_end` here would instead eat a blank separator line
|
|
115
113
|
// that legitimately belongs between statements.
|
|
116
114
|
if let Some(source_text) = ctx.extract_source(node) {
|
|
117
|
-
|
|
118
|
-
let reformatted = reformat_chain_lines(
|
|
119
|
-
source_text,
|
|
120
|
-
base_indent,
|
|
121
|
-
ctx.config().formatting.indent_width,
|
|
122
|
-
);
|
|
123
|
-
let trimmed = strip_one_trailing_newline(&reformatted);
|
|
124
|
-
docs.push(text(trimmed.to_string()));
|
|
115
|
+
docs.push(chain_doc_or_verbatim(source_text));
|
|
125
116
|
}
|
|
126
117
|
|
|
127
118
|
// Mark comments in this range as emitted (they're in source extraction)
|
|
@@ -148,15 +139,16 @@ fn format_call(node: &Node, ctx: &mut FormatContext, registry: &RuleRegistry) ->
|
|
|
148
139
|
.source()
|
|
149
140
|
.get(node.location.start_offset..call_end_offset)
|
|
150
141
|
{
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
142
|
+
match reformat_chain_doc(call_text.trim_end()) {
|
|
143
|
+
Some(chain_doc) => {
|
|
144
|
+
docs.push(chain_doc);
|
|
145
|
+
true
|
|
146
|
+
}
|
|
147
|
+
None => {
|
|
148
|
+
docs.push(text(call_text.trim_end().to_string()));
|
|
149
|
+
false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
160
152
|
} else {
|
|
161
153
|
false
|
|
162
154
|
};
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
//! net for node types that haven't been implemented yet.
|
|
6
6
|
|
|
7
7
|
use crate::ast::Node;
|
|
8
|
-
use crate::doc::{concat,
|
|
8
|
+
use crate::doc::{concat, Doc};
|
|
9
9
|
use crate::error::Result;
|
|
10
10
|
use crate::format::context::FormatContext;
|
|
11
11
|
use crate::format::registry::RuleRegistry;
|
|
12
12
|
use crate::format::rule::{
|
|
13
|
-
format_leading_comments, format_trailing_comment,
|
|
14
|
-
mark_comments_in_range_emitted,
|
|
13
|
+
chain_doc_or_verbatim, format_leading_comments, format_trailing_comment,
|
|
14
|
+
mark_comments_in_range_emitted, FormatRule,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/// Fallback rule that extracts source text directly.
|
|
@@ -47,14 +47,7 @@ impl FormatRule for FallbackRule {
|
|
|
47
47
|
// could swallow an intentional blank line captured by the node's
|
|
48
48
|
// extent).
|
|
49
49
|
if let Some(source_text) = ctx.extract_source(node) {
|
|
50
|
-
|
|
51
|
-
let reformatted = reformat_chain_lines(
|
|
52
|
-
source_text,
|
|
53
|
-
base_indent,
|
|
54
|
-
ctx.config().formatting.indent_width,
|
|
55
|
-
);
|
|
56
|
-
let trimmed = strip_one_trailing_newline(&reformatted);
|
|
57
|
-
docs.push(text(trimmed.to_string()));
|
|
50
|
+
docs.push(chain_doc_or_verbatim(source_text));
|
|
58
51
|
|
|
59
52
|
// Mark any comments within this node's range as emitted
|
|
60
53
|
// (they are included in the source extraction)
|
|
@@ -14,7 +14,7 @@ use crate::format::context::FormatContext;
|
|
|
14
14
|
use crate::format::registry::RuleRegistry;
|
|
15
15
|
use crate::format::rule::{
|
|
16
16
|
format_leading_comments, format_statements, format_trailing_comment,
|
|
17
|
-
strip_one_trailing_newline, FormatRule,
|
|
17
|
+
mark_comments_in_range_emitted, strip_one_trailing_newline, FormatRule,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
/// Rule for formatting if conditionals.
|
|
@@ -115,8 +115,13 @@ fn format_postfix(
|
|
|
115
115
|
// the modifier is already baked into the source text right where
|
|
116
116
|
// Ruby expects it.
|
|
117
117
|
if let Some(statements) = node.children.get(1) {
|
|
118
|
-
if let Some(source_text) = ctx.extract_source(statements) {
|
|
119
|
-
|
|
118
|
+
if let Some(source_text) = ctx.extract_source(statements).map(str::to_string) {
|
|
119
|
+
mark_comments_in_range_emitted(
|
|
120
|
+
ctx,
|
|
121
|
+
statements.location.start_line,
|
|
122
|
+
statements.location.end_line,
|
|
123
|
+
);
|
|
124
|
+
if statement_contains_heredoc_tail(&source_text) {
|
|
120
125
|
docs.push(text(source_text.trim_end_matches('\n').to_string()));
|
|
121
126
|
|
|
122
127
|
let trailing = format_trailing_comment(ctx, node.location.end_line);
|
|
@@ -193,6 +198,11 @@ fn format_ternary(node: &Node, ctx: &mut FormatContext) -> Result<Doc> {
|
|
|
193
198
|
if let Some(statements) = node.children.get(1) {
|
|
194
199
|
if let Some(source_text) = ctx.extract_source(statements) {
|
|
195
200
|
docs.push(text(source_text.trim()));
|
|
201
|
+
mark_comments_in_range_emitted(
|
|
202
|
+
ctx,
|
|
203
|
+
statements.location.start_line,
|
|
204
|
+
statements.location.end_line,
|
|
205
|
+
);
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
208
|
|
|
@@ -203,6 +213,11 @@ fn format_ternary(node: &Node, ctx: &mut FormatContext) -> Result<Doc> {
|
|
|
203
213
|
if let Some(else_statements) = else_node.children.first() {
|
|
204
214
|
if let Some(source_text) = ctx.extract_source(else_statements) {
|
|
205
215
|
docs.push(text(source_text.trim()));
|
|
216
|
+
mark_comments_in_range_emitted(
|
|
217
|
+
ctx,
|
|
218
|
+
else_statements.location.start_line,
|
|
219
|
+
else_statements.location.end_line,
|
|
220
|
+
);
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
}
|
|
@@ -322,6 +337,13 @@ fn format_normal(
|
|
|
322
337
|
docs.push(hardline());
|
|
323
338
|
docs.push(text("else"));
|
|
324
339
|
|
|
340
|
+
// Trailing comment on the else line (ElseNode starts at the
|
|
341
|
+
// `else` keyword)
|
|
342
|
+
let else_trailing = format_trailing_comment(ctx, consequent.location.start_line);
|
|
343
|
+
if !else_trailing.is_empty() {
|
|
344
|
+
docs.push(else_trailing);
|
|
345
|
+
}
|
|
346
|
+
|
|
325
347
|
// Emit else body (first child of ElseNode)
|
|
326
348
|
if let Some(else_statements) = consequent.children.first() {
|
|
327
349
|
if matches!(else_statements.node_type, NodeType::StatementsNode) {
|
|
@@ -12,7 +12,8 @@ use crate::error::Result;
|
|
|
12
12
|
use crate::format::context::FormatContext;
|
|
13
13
|
use crate::format::registry::RuleRegistry;
|
|
14
14
|
use crate::format::rule::{
|
|
15
|
-
format_leading_comments, format_statements, format_trailing_comment,
|
|
15
|
+
format_leading_comments, format_statements, format_trailing_comment,
|
|
16
|
+
mark_comments_in_range_emitted, FormatRule,
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
/// Rule for formatting while loops.
|
|
@@ -80,6 +81,7 @@ fn format_postfix_while_until(node: &Node, ctx: &mut FormatContext, keyword: &st
|
|
|
80
81
|
// Extract from source for postfix form
|
|
81
82
|
if let Some(source_text) = ctx.extract_source(node) {
|
|
82
83
|
docs.push(text(source_text));
|
|
84
|
+
mark_comments_in_range_emitted(ctx, node.location.start_line, node.location.end_line);
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
// Trailing comment
|
|
@@ -12,8 +12,8 @@ use crate::error::Result;
|
|
|
12
12
|
use crate::format::context::FormatContext;
|
|
13
13
|
use crate::format::registry::RuleRegistry;
|
|
14
14
|
use crate::format::rule::{
|
|
15
|
-
format_child, format_leading_comments, format_trailing_comment,
|
|
16
|
-
|
|
15
|
+
chain_doc_or_verbatim, format_child, format_leading_comments, format_trailing_comment,
|
|
16
|
+
mark_comments_in_range_emitted, FormatRule,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
/// Rule for formatting local variable write expressions.
|
|
@@ -66,6 +66,7 @@ fn format_variable_write(
|
|
|
66
66
|
// No value: fallback to source extraction
|
|
67
67
|
if let Some(source_text) = ctx.extract_source(node) {
|
|
68
68
|
docs.push(text(source_text.to_string()));
|
|
69
|
+
mark_comments_in_range_emitted(ctx, start_line, end_line);
|
|
69
70
|
}
|
|
70
71
|
let trailing = format_trailing_comment(ctx, end_line);
|
|
71
72
|
if !trailing.is_empty() {
|
|
@@ -124,19 +125,25 @@ fn format_variable_write(
|
|
|
124
125
|
// (`x = foo(<<~SQL)\n…\nSQL\n`) so it doesn't compound with the
|
|
125
126
|
// surrounding hardline.
|
|
126
127
|
if let Some(source_text) = ctx.extract_source(value) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
docs.push(chain_doc_or_verbatim(source_text.trim_start()));
|
|
129
|
+
// The extracted text carries any interior comments verbatim
|
|
130
|
+
// (e.g. inside a `do…end` block); without marking them they
|
|
131
|
+
// get re-emitted at EOF by format_remaining_comments.
|
|
132
|
+
mark_comments_in_range_emitted(
|
|
133
|
+
ctx,
|
|
134
|
+
value.location.start_line,
|
|
135
|
+
value.location.end_line,
|
|
132
136
|
);
|
|
133
|
-
let trimmed = strip_one_trailing_newline(reformatted.trim_start());
|
|
134
|
-
docs.push(text(trimmed.to_string()));
|
|
135
137
|
}
|
|
136
138
|
} else {
|
|
137
139
|
// Simple value: extract from source trimmed
|
|
138
140
|
if let Some(source_text) = ctx.extract_source(value) {
|
|
139
141
|
docs.push(text(source_text.trim().to_string()));
|
|
142
|
+
mark_comments_in_range_emitted(
|
|
143
|
+
ctx,
|
|
144
|
+
value.location.start_line,
|
|
145
|
+
value.location.end_line,
|
|
146
|
+
);
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
}
|