Skip to content

Commit ee23fbe

Browse files
authored
Merge pull request #4223 from eregon/newline-test
Remove all remaining skips in newline_test.rb
2 parents 4651adb + 2299cc9 commit ee23fbe

7 files changed

Lines changed: 742 additions & 56 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ out.svg
4242
/javascript/src/visitor.js
4343
/javascript/src/prism.wasm
4444
/javascript/src/*.d.ts
45+
/java/newline_fixtures.txt
4546
/java/org/ruby_lang/prism/AbstractNodeVisitor.java
4647
/java/org/ruby_lang/prism/Loader.java
4748
/java/org/ruby_lang/prism/Nodes.java

java/api/src/main/java/org/ruby_lang/prism/MarkNewlinesVisitor.java

Lines changed: 253 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.ruby_lang.prism;
22

3+
import java.util.Arrays;
4+
35
// Keep in sync with Ruby MarkNewlinesVisitor
46
final class MarkNewlinesVisitor extends AbstractNodeVisitor<Void> {
57

@@ -33,26 +35,274 @@ public Void visitLambdaNode(Nodes.LambdaNode node) {
3335
}
3436
}
3537

38+
@Override
39+
public Void visitDefNode(Nodes.DefNode node) {
40+
boolean[] oldNewlineMarked = this.newlineMarked;
41+
this.newlineMarked = new boolean[oldNewlineMarked.length];
42+
// The body of an endless method definition never emits newline events,
43+
// so in that case mark every line as already seen instead. There is no
44+
// location for the `=` operator here, but only an endless method
45+
// definition has a statements body which ends with the def node itself.
46+
if (node.body instanceof Nodes.StatementsNode && node.endOffset() == node.body.endOffset()) {
47+
Arrays.fill(this.newlineMarked, true);
48+
}
49+
try {
50+
return super.visitDefNode(node);
51+
} finally {
52+
this.newlineMarked = oldNewlineMarked;
53+
}
54+
}
55+
56+
@Override
57+
public Void visitClassNode(Nodes.ClassNode node) {
58+
boolean[] oldNewlineMarked = this.newlineMarked;
59+
this.newlineMarked = new boolean[oldNewlineMarked.length];
60+
try {
61+
return super.visitClassNode(node);
62+
} finally {
63+
this.newlineMarked = oldNewlineMarked;
64+
}
65+
}
66+
67+
@Override
68+
public Void visitModuleNode(Nodes.ModuleNode node) {
69+
boolean[] oldNewlineMarked = this.newlineMarked;
70+
this.newlineMarked = new boolean[oldNewlineMarked.length];
71+
try {
72+
return super.visitModuleNode(node);
73+
} finally {
74+
this.newlineMarked = oldNewlineMarked;
75+
}
76+
}
77+
78+
@Override
79+
public Void visitSingletonClassNode(Nodes.SingletonClassNode node) {
80+
boolean[] oldNewlineMarked = this.newlineMarked;
81+
this.newlineMarked = new boolean[oldNewlineMarked.length];
82+
try {
83+
return super.visitSingletonClassNode(node);
84+
} finally {
85+
this.newlineMarked = oldNewlineMarked;
86+
}
87+
}
88+
89+
// Statements inside string interpolation do not emit newline events, so
90+
// mark every line as already seen while visiting them. Nested scopes
91+
// (blocks, lambdas, defs, etc.) reset the lines and emit events again.
92+
@Override
93+
public Void visitEmbeddedStatementsNode(Nodes.EmbeddedStatementsNode node) {
94+
boolean[] oldNewlineMarked = this.newlineMarked;
95+
this.newlineMarked = new boolean[oldNewlineMarked.length];
96+
Arrays.fill(this.newlineMarked, true);
97+
try {
98+
return super.visitEmbeddedStatementsNode(node);
99+
} finally {
100+
this.newlineMarked = oldNewlineMarked;
101+
}
102+
}
103+
104+
// The predicate of a while loop is compiled at the end of the loop,
105+
// after the body, so any statements it contains (from parentheses)
106+
// emit their line events again even if the lines were already seen.
107+
@Override
108+
public Void visitWhileNode(Nodes.WhileNode node) {
109+
boolean[] oldNewlineMarked = this.newlineMarked;
110+
this.newlineMarked = new boolean[oldNewlineMarked.length];
111+
try {
112+
node.predicate.accept(this);
113+
} finally {
114+
this.newlineMarked = oldNewlineMarked;
115+
}
116+
117+
if (node.statements != null) {
118+
node.statements.accept(this);
119+
}
120+
return null;
121+
}
122+
123+
// The predicate of an until loop is compiled at the end of the loop,
124+
// after the body, so any statements it contains (from parentheses)
125+
// emit their line events again even if the lines were already seen.
126+
@Override
127+
public Void visitUntilNode(Nodes.UntilNode node) {
128+
boolean[] oldNewlineMarked = this.newlineMarked;
129+
this.newlineMarked = new boolean[oldNewlineMarked.length];
130+
try {
131+
node.predicate.accept(this);
132+
} finally {
133+
this.newlineMarked = oldNewlineMarked;
134+
}
135+
136+
if (node.statements != null) {
137+
node.statements.accept(this);
138+
}
139+
return null;
140+
}
141+
36142
@Override
37143
public Void visitIfNode(Nodes.IfNode node) {
38-
node.setNewLineFlag(this.source, this.newlineMarked);
144+
setNewLineFlag(node);
39145
return super.visitIfNode(node);
40146
}
41147

42148
@Override
43149
public Void visitUnlessNode(Nodes.UnlessNode node) {
44-
node.setNewLineFlag(this.source, this.newlineMarked);
150+
setNewLineFlag(node);
45151
return super.visitUnlessNode(node);
46152
}
47153

48154
@Override
49155
public Void visitStatementsNode(Nodes.StatementsNode node) {
50156
for (Nodes.Node child : node.body) {
51-
child.setNewLineFlag(this.source, this.newlineMarked);
157+
setNewLineFlag(child);
52158
}
53159
return super.visitStatementsNode(node);
54160
}
55161

162+
// Keep in sync with the newline_flag! overrides in Ruby's newlines.rb which
163+
// are not part of the generated setNewLineFlag() methods.
164+
//
165+
// The line event for a statement is emitted where its first instruction is
166+
// compiled, so nodes whose first instruction comes from a sub-expression
167+
// delegate their newline flag to that sub-expression: assignments to their
168+
// value, calls to their receiver, and array, hash, and interpolated string
169+
// literals to their first element. Static literals are the exception: they
170+
// are compiled to a single instruction on the first line of the literal, so
171+
// they do not delegate.
172+
private void setNewLineFlag(Nodes.Node node) {
173+
if (node instanceof Nodes.LocalVariableWriteNode write) {
174+
setNewLineFlag(write.value);
175+
} else if (node instanceof Nodes.InstanceVariableWriteNode write) {
176+
setNewLineFlag(write.value);
177+
} else if (node instanceof Nodes.ClassVariableWriteNode write) {
178+
setNewLineFlag(write.value);
179+
} else if (node instanceof Nodes.GlobalVariableWriteNode write) {
180+
setNewLineFlag(write.value);
181+
} else if (node instanceof Nodes.ConstantWriteNode write) {
182+
setNewLineFlag(write.value);
183+
} else if (node instanceof Nodes.ConstantPathWriteNode write) {
184+
setNewLineFlag(write.value);
185+
} else if (node instanceof Nodes.MultiWriteNode write) {
186+
setNewLineFlag(write.value);
187+
} else if (node instanceof Nodes.CallNode call) {
188+
if (call.receiver != null) {
189+
setNewLineFlag(call.receiver);
190+
} else {
191+
markNewLineFlag(node);
192+
}
193+
} else if (node instanceof Nodes.ArrayNode array) {
194+
if (array.elements.length > 0 && !isStaticLiteral(array)) {
195+
setNewLineFlag(array.elements[0]);
196+
} else {
197+
markNewLineFlag(node);
198+
}
199+
} else if (node instanceof Nodes.HashNode hash) {
200+
if (hash.elements.length > 0 && !isStaticLiteral(hash)) {
201+
setNewLineFlag(hash.elements[0]);
202+
} else {
203+
markNewLineFlag(node);
204+
}
205+
} else if (node instanceof Nodes.InterpolatedStringNode string) {
206+
if (string.parts.length > 0 && !isStaticLiteral(string)) {
207+
setNewLineFlag(string.parts[0]);
208+
} else {
209+
markNewLineFlag(node);
210+
}
211+
} else if (node instanceof Nodes.IfNode ifNode) {
212+
setNewLineFlag(ifNode.predicate);
213+
} else if (node instanceof Nodes.UnlessNode unlessNode) {
214+
setNewLineFlag(unlessNode.predicate);
215+
} else if (node instanceof Nodes.RescueModifierNode rescueModifier) {
216+
setNewLineFlag(rescueModifier.expression);
217+
} else if (node instanceof Nodes.WhileNode whileNode) {
218+
boolean prefix = isPrefixLoop(whileNode, whileNode.isBeginModifier(), whileNode.statements);
219+
if (prefix && whileNode.predicate instanceof Nodes.ParenthesesNode) {
220+
// A parenthesized predicate emits its own line event when it is
221+
// compiled at the end of the loop, in addition to this one.
222+
markNewLineFlag(node);
223+
} else {
224+
setNewLineFlag(whileNode.predicate);
225+
}
226+
} else if (node instanceof Nodes.UntilNode untilNode) {
227+
boolean prefix = isPrefixLoop(untilNode, untilNode.isBeginModifier(), untilNode.statements);
228+
if (prefix && untilNode.predicate instanceof Nodes.ParenthesesNode) {
229+
// A parenthesized predicate emits its own line event when it is
230+
// compiled at the end of the loop, in addition to this one.
231+
markNewLineFlag(node);
232+
} else {
233+
setNewLineFlag(untilNode.predicate);
234+
}
235+
} else {
236+
node.setNewLineFlag(this.source, this.newlineMarked);
237+
}
238+
}
239+
240+
// Mark the node itself, like Nodes.Node#setNewLineFlag(), regardless of any
241+
// setNewLineFlag() override of the node.
242+
private void markNewLineFlag(Nodes.Node node) {
243+
int line = this.source.findLine(node.startOffset);
244+
if (!this.newlineMarked[line]) {
245+
this.newlineMarked[line] = true;
246+
node.setNewLineFlag(true);
247+
}
248+
}
249+
250+
// PM_NODE_FLAG_STATIC_LITERAL, which is serialized together with the
251+
// node-specific flags.
252+
private static final short STATIC_LITERAL_FLAG = 0x2;
253+
254+
// Whether the node has the PM_NODE_FLAG_STATIC_LITERAL flag. Only nodes
255+
// with node-specific flags store their flags in Java, so it is computed
256+
// structurally for the others, for the nodes which can appear inside an
257+
// array or hash literal.
258+
private static boolean isStaticLiteral(Nodes.Node node) {
259+
if (node instanceof Nodes.ArrayNode array) {
260+
return (array.flags & STATIC_LITERAL_FLAG) != 0;
261+
} else if (node instanceof Nodes.InterpolatedStringNode string) {
262+
return (string.flags & STATIC_LITERAL_FLAG) != 0;
263+
} else if (node instanceof Nodes.StringNode string) {
264+
return (string.flags & STATIC_LITERAL_FLAG) != 0;
265+
} else if (node instanceof Nodes.SymbolNode symbol) {
266+
return (symbol.flags & STATIC_LITERAL_FLAG) != 0;
267+
} else if (node instanceof Nodes.IntegerNode integer) {
268+
return (integer.flags & STATIC_LITERAL_FLAG) != 0;
269+
} else if (node instanceof Nodes.RationalNode rational) {
270+
return (rational.flags & STATIC_LITERAL_FLAG) != 0;
271+
} else if (node instanceof Nodes.RegularExpressionNode regexp) {
272+
return (regexp.flags & STATIC_LITERAL_FLAG) != 0;
273+
} else if (node instanceof Nodes.RangeNode range) {
274+
return (range.flags & STATIC_LITERAL_FLAG) != 0;
275+
} else if (node instanceof Nodes.HashNode hash) {
276+
for (Nodes.Node element : hash.elements) {
277+
if (!(element instanceof Nodes.AssocNode assoc)) {
278+
return false;
279+
}
280+
// An assoc node with a container key or value is never a static literal.
281+
if (isContainer(assoc.key) || isContainer(assoc.value)) {
282+
return false;
283+
}
284+
if (!isStaticLiteral(assoc.key) || !isStaticLiteral(assoc.value)) {
285+
return false;
286+
}
287+
}
288+
return true;
289+
} else {
290+
return node instanceof Nodes.NilNode || node instanceof Nodes.TrueNode || node instanceof Nodes.FalseNode ||
291+
node instanceof Nodes.FloatNode || node instanceof Nodes.ImaginaryNode || node instanceof Nodes.SourceLineNode;
292+
}
293+
}
294+
295+
private static boolean isContainer(Nodes.Node node) {
296+
return node instanceof Nodes.ArrayNode || node instanceof Nodes.HashNode || node instanceof Nodes.RangeNode;
297+
}
298+
299+
// Whether a while/until loop starts with its keyword. There is no keyword
300+
// location in the Java nodes, but only a prefix loop starts before its
301+
// statements.
302+
private static boolean isPrefixLoop(Nodes.Node node, boolean beginModifier, Nodes.StatementsNode statements) {
303+
return !beginModifier && (statements == null || node.startOffset != statements.startOffset);
304+
}
305+
56306
@Override
57307
protected Void defaultVisit(Nodes.Node node) {
58308
node.visitChildNodes(this);

0 commit comments

Comments
 (0)