Skip to content

Commit 3c1e75a

Browse files
committed
[fix] indentation & trailing whitespace in Heredoc
An attempt at fixing a few Heredoc issues: * 1 tab equals 8 spaces * 1 space + 1 tab equals 8 spaces * Trailing whitespace in squiggly Heredoc should not be stripped. * A single whitespace-only line should be preserved * Multiple whitespace-only lines should be preserved See: https://docs.ruby-lang.org/en/master/syntax/literals_rdoc.html#here-document-literals >The indentation of the least-indented line will be removed from each >line of the content. Note that empty lines and lines consisting solely >of literal tabs and spaces will be ignored for the purposes of >determining indentation, but escaped tabs and spaces are considered >non-indentation characters. > >For the purpose of measuring an indentation, a horizontal tab is >regarded as a sequence of one to eight spaces such that the column >position corresponding to its end is a multiple of eight. The amount to >be removed is counted in terms of the number of spaces. If the boundary >appears in the middle of a tab, that tab is not removed.
1 parent d3d433c commit 3c1e75a

5 files changed

Lines changed: 305 additions & 19 deletions

File tree

ci/string_literals_stress_test.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,75 @@ def foo
118118
puts <<EOD.gsub("a", "b")
119119
"cde"
120120
EOD
121+
122+
# whitespace-only lines are not considered when calculating indentation
123+
puts \
124+
<<~FOO.inspect
125+
the next line contains a single space
126+
127+
FOO
128+
129+
# 1 tab equals 8 spaces, both should be considered indentation
130+
puts \
131+
<<~FOO.inspect
132+
this line is indented 8 spaces
133+
this line is indented 1 tab
134+
FOO
135+
136+
# 1 tab plus 1 space equals 8 spaces
137+
puts \
138+
<<~FOO.inspect
139+
this line is indented 9 spaces
140+
this line is indented 1 space and 1 tab
141+
FOO
142+
143+
# 1 tab is greater than 7 spaces, the tab should be considered content
144+
puts \
145+
<<~FOO.inspect
146+
this line is indented 7 spaces
147+
this line is indented 1 tab
148+
FOO
149+
150+
# trailing spaces should not be removed
151+
puts \
152+
<<-FOO.inspect
153+
this line and the next have a trailing space:
154+
155+
this line and the next have a trailing tab:
156+
157+
FOO
158+
159+
# trailing spaces should not be removed (squiggly)
160+
puts \
161+
<<~FOO.inspect
162+
this line and the next have a trailing space:
163+
164+
this line and the next have a trailing tab:
165+
166+
FOO
167+
168+
# single line of trailing space
169+
puts \
170+
<<-FOO.inspect
171+
172+
FOO
173+
174+
# single line of trailing space (squiggly)
175+
puts \
176+
<<~FOO.inspect
177+
178+
FOO
179+
180+
# multiple lines of trailing space
181+
puts \
182+
<<-FOO.inspect
183+
184+
185+
FOO
186+
187+
# multiple lines of trailing space (squiggly)
188+
puts \
189+
<<~FOO.inspect
190+
191+
192+
FOO

fixtures/small/heredoc_indented_whitespace_actual.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,75 @@ def foo
1313
hi there
1414
THING
1515
end
16+
17+
# whitespace-only lines are not considered when calculating indentation
18+
puts \
19+
<<~FOO.inspect
20+
the next line contains a single space
21+
22+
FOO
23+
24+
# 1 tab equals 8 spaces, both should be considered indentation
25+
puts \
26+
<<~FOO.inspect
27+
this line is indented 8 spaces
28+
this line is indented 1 tab
29+
FOO
30+
31+
# 1 tab plus 1 space equals 8 spaces
32+
puts \
33+
<<~FOO.inspect
34+
this line is indented 9 spaces
35+
this line is indented 1 space and 1 tab
36+
FOO
37+
38+
# 1 tab is greater than 7 spaces, the tab should be considered content
39+
puts \
40+
<<~FOO.inspect
41+
this line is indented 7 spaces
42+
this line is indented 1 tab
43+
FOO
44+
45+
# trailing spaces should not be removed
46+
puts \
47+
<<-FOO.inspect
48+
this line and the next have a trailing space:
49+
50+
this line and the next have a trailing tab:
51+
52+
FOO
53+
54+
# trailing spaces should not be removed (squiggly)
55+
puts \
56+
<<~FOO.inspect
57+
this line and the next have a trailing space:
58+
59+
this line and the next have a trailing tab:
60+
61+
FOO
62+
63+
# single line of trailing space
64+
puts \
65+
<<-FOO.inspect
66+
67+
FOO
68+
69+
# single line of trailing space (squiggly)
70+
puts \
71+
<<~FOO.inspect
72+
73+
FOO
74+
75+
# multiple lines of trailing space
76+
puts \
77+
<<-FOO.inspect
78+
79+
80+
FOO
81+
82+
# multiple lines of trailing space (squiggly)
83+
puts \
84+
<<~FOO.inspect
85+
86+
87+
FOO
Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,107 @@
11
def foo
22
<<~THING
3-
awfweaf
3+
awfweaf
44
awefawef
5-
6-
7-
5+
6+
7+
88
hi there
99
THING
1010

1111
<<-THING
12-
12+
1313
hi there
1414
THING
1515
end
16+
17+
# whitespace-only lines are not considered when calculating indentation
18+
puts(
19+
<<~FOO
20+
the next line contains a single space
21+
22+
FOO
23+
.inspect
24+
)
25+
26+
# 1 tab equals 8 spaces, both should be considered indentation
27+
puts(
28+
<<~FOO
29+
this line is indented 8 spaces
30+
this line is indented 1 tab
31+
FOO
32+
.inspect
33+
)
34+
35+
# 1 tab plus 1 space equals 8 spaces
36+
puts(
37+
<<~FOO
38+
this line is indented 9 spaces
39+
this line is indented 1 space and 1 tab
40+
FOO
41+
.inspect
42+
)
43+
44+
# 1 tab is greater than 7 spaces, the tab should be considered content
45+
puts(
46+
<<~FOO
47+
this line is indented 7 spaces
48+
this line is indented 1 tab
49+
FOO
50+
.inspect
51+
)
52+
53+
# trailing spaces should not be removed
54+
puts(
55+
<<-FOO
56+
this line and the next have a trailing space:
57+
58+
this line and the next have a trailing tab:
59+
60+
FOO
61+
.inspect
62+
)
63+
64+
# trailing spaces should not be removed (squiggly)
65+
puts(
66+
<<~FOO
67+
this line and the next have a trailing space:
68+
69+
this line and the next have a trailing tab:
70+
71+
FOO
72+
.inspect
73+
)
74+
75+
# single line of trailing space
76+
puts(
77+
<<-FOO
78+
79+
FOO
80+
.inspect
81+
)
82+
83+
# single line of trailing space (squiggly)
84+
puts(
85+
<<~FOO
86+
87+
FOO
88+
.inspect
89+
)
90+
91+
# multiple lines of trailing space
92+
puts(
93+
<<-FOO
94+
95+
96+
FOO
97+
.inspect
98+
)
99+
100+
# multiple lines of trailing space (squiggly)
101+
puts(
102+
<<~FOO
103+
104+
105+
FOO
106+
.inspect
107+
)

librubyfmt/src/format_prism.rs

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -931,9 +931,12 @@ fn format_inner_string<'src>(
931931
.count()
932932
- escaped_ws_count;
933933

934+
let raw_leading_cols = count_indent_cols(&raw[..raw_leading]);
935+
let unescaped_leading_cols = count_indent_cols(&unescaped[..unescaped_leading]);
936+
934937
// The difference is the common indent (if raw has more leading whitespace)
935-
if raw_leading > unescaped_leading {
936-
return Some(raw_leading - unescaped_leading);
938+
if raw_leading_cols > unescaped_leading_cols {
939+
return Some(raw_leading_cols - unescaped_leading_cols);
937940
}
938941
}
939942
None
@@ -960,11 +963,10 @@ fn format_inner_string<'src>(
960963
.enumerate()
961964
.map(|(line_idx, line)| {
962965
// Strip from lines at line boundaries
963-
let should_strip = (line_idx > 0 || prev_ended_with_newline)
964-
&& !line.is_empty()
965-
&& line.len() >= common_indent;
966+
let should_strip =
967+
(line_idx > 0 || prev_ended_with_newline) && !line.is_empty();
966968
if should_strip {
967-
&line[common_indent..]
969+
strip_indent_cols(line, common_indent)
968970
} else {
969971
line
970972
}
@@ -5167,3 +5169,42 @@ fn format_write_node<'src>(
51675169
ps.emit_space();
51685170
ps.with_start_of_line(false, |ps| format_node(ps, value));
51695171
}
5172+
5173+
/// Count leading whitespace columns. Tabs advance to the next multiple of 8.
5174+
fn count_indent_cols(line: &[u8]) -> usize {
5175+
let mut col = 0;
5176+
for &b in line {
5177+
match b {
5178+
b' ' => col += 1,
5179+
b'\t' => col = (col / 8 + 1) * 8,
5180+
_ => break,
5181+
}
5182+
}
5183+
col
5184+
}
5185+
5186+
/// Strip up to `cols` leading whitespace columns from `line`.
5187+
/// Tabs advance to the next multiple of 8.
5188+
/// If a tab would advance past `cols`, it is not stripped.
5189+
fn strip_indent_cols(line: &[u8], cols: usize) -> &[u8] {
5190+
let mut col = 0;
5191+
let mut i = 0;
5192+
while i < line.len() && col < cols {
5193+
match line[i] {
5194+
b' ' => {
5195+
col += 1;
5196+
i += 1;
5197+
}
5198+
b'\t' => {
5199+
col = (col / 8 + 1) * 8;
5200+
if col > cols {
5201+
// We are in the middle of a tab. Do not strip it.
5202+
break;
5203+
}
5204+
i += 1;
5205+
}
5206+
_ => break,
5207+
}
5208+
}
5209+
&line[i..]
5210+
}

librubyfmt/src/heredoc_string.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,16 @@ impl<'src> HeredocString<'src> {
6868
let indent = self.indent;
6969

7070
if self.kind.is_squiggly() {
71-
// For squiggly heredocs, we need to apply indentation to Normal segments
72-
// but not to Raw segments (which come from nested non-squiggly heredocs).
71+
// Do not indent raw segments (nested non-squiggly heredocs).
72+
// Do not indent when all normal segments contain only whitespace.
73+
let should_indent = self.segments.iter().any(|seg| {
74+
if let HeredocSegment::Normal(c) = seg {
75+
c.iter().any(|&b| b != b' ' && b != b'\t' && b != b'\n')
76+
} else {
77+
false
78+
}
79+
});
80+
7381
let mut result = Vec::new();
7482
for segment in self.segments {
7583
match segment {
@@ -79,9 +87,10 @@ impl<'src> HeredocString<'src> {
7987
if i > 0 {
8088
result.push(b'\n');
8189
}
82-
let mut indented = get_indent(indent as usize + 2).into_owned();
83-
indented.extend_from_slice(line);
84-
result.extend_from_slice(indented.trim_ascii_end());
90+
if !line.is_empty() && should_indent {
91+
result.extend_from_slice(get_indent(indent as usize + 2).as_ref());
92+
}
93+
result.extend_from_slice(line);
8594
}
8695
}
8796
HeredocSegment::Raw(content) => {
@@ -90,14 +99,14 @@ impl<'src> HeredocString<'src> {
9099
if i > 0 {
91100
result.push(b'\n');
92101
}
93-
result.extend_from_slice(line.trim_ascii_end());
102+
result.extend_from_slice(line);
94103
}
95104
}
96105
}
97106
}
98107
result
99108
} else {
100-
// For non-squiggly heredocs, just join segments and trim line endings
109+
// For non-squiggly heredocs, just join segments
101110
let mut result = Vec::new();
102111
for segment in self.segments {
103112
let content = match segment {
@@ -107,7 +116,7 @@ impl<'src> HeredocString<'src> {
107116
if i > 0 {
108117
result.push(b'\n');
109118
}
110-
result.extend_from_slice(line.trim_ascii_end());
119+
result.extend_from_slice(line);
111120
}
112121
}
113122
result

0 commit comments

Comments
 (0)