|
1 | 1 | package org.ruby_lang.prism; |
2 | 2 |
|
| 3 | +import java.util.Arrays; |
| 4 | + |
3 | 5 | // Keep in sync with Ruby MarkNewlinesVisitor |
4 | 6 | final class MarkNewlinesVisitor extends AbstractNodeVisitor<Void> { |
5 | 7 |
|
@@ -33,26 +35,274 @@ public Void visitLambdaNode(Nodes.LambdaNode node) { |
33 | 35 | } |
34 | 36 | } |
35 | 37 |
|
| 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 | + |
36 | 142 | @Override |
37 | 143 | public Void visitIfNode(Nodes.IfNode node) { |
38 | | - node.setNewLineFlag(this.source, this.newlineMarked); |
| 144 | + setNewLineFlag(node); |
39 | 145 | return super.visitIfNode(node); |
40 | 146 | } |
41 | 147 |
|
42 | 148 | @Override |
43 | 149 | public Void visitUnlessNode(Nodes.UnlessNode node) { |
44 | | - node.setNewLineFlag(this.source, this.newlineMarked); |
| 150 | + setNewLineFlag(node); |
45 | 151 | return super.visitUnlessNode(node); |
46 | 152 | } |
47 | 153 |
|
48 | 154 | @Override |
49 | 155 | public Void visitStatementsNode(Nodes.StatementsNode node) { |
50 | 156 | for (Nodes.Node child : node.body) { |
51 | | - child.setNewLineFlag(this.source, this.newlineMarked); |
| 157 | + setNewLineFlag(child); |
52 | 158 | } |
53 | 159 | return super.visitStatementsNode(node); |
54 | 160 | } |
55 | 161 |
|
| 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 | + |
56 | 306 | @Override |
57 | 307 | protected Void defaultVisit(Nodes.Node node) { |
58 | 308 | node.visitChildNodes(this); |
|
0 commit comments