1 /// Component for adding imports to a file, reading imports at a location of code and sorting imports. 2 module workspaced.com.importer; 3 4 import dparse.ast; 5 import dparse.lexer; 6 import dparse.parser; 7 import dparse.rollback_allocator; 8 9 import std.algorithm; 10 import std.array; 11 import std.functional; 12 import std.stdio; 13 import std.string; 14 import std.uni : sicmp; 15 16 import workspaced.api; 17 import workspaced.helpers : determineIndentation, endsWithKeyword, 18 indexOfKeyword, stripLineEndingLength; 19 20 /// ditto 21 @component("importer") 22 class ImporterComponent : ComponentWrapper 23 { 24 mixin DefaultComponentWrapper; 25 26 protected void load() 27 { 28 config.stringBehavior = StringBehavior.source; 29 } 30 31 /// Returns all imports available at some code position. 32 ImportInfo[] get(scope const(char)[] code, int pos) 33 { 34 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 35 auto mod = parseModule(tokens, "code", &rba); 36 auto reader = new ImporterReaderVisitor(pos); 37 reader.visit(mod); 38 return reader.imports; 39 } 40 41 /// Returns a list of code patches for adding an import. 42 /// If `insertOutermost` is false, the import will get added to the innermost block. 43 ImportModification add(string importName, scope const(char)[] code, int pos, 44 bool insertOutermost = true) 45 { 46 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 47 auto mod = parseModule(tokens, "code", &rba); 48 auto reader = new ImporterReaderVisitor(pos); 49 reader.visit(mod); 50 foreach (i; reader.imports) 51 { 52 if (i.name.join('.') == importName) 53 { 54 if (i.selectives.length == 0) 55 return ImportModification(i.rename, []); 56 else 57 insertOutermost = false; 58 } 59 } 60 string indentation = ""; 61 if (insertOutermost) 62 { 63 indentation = reader.outerImportLocation == 0 ? "" : (cast(ubyte[]) code) 64 .getIndentation(reader.outerImportLocation); 65 if (reader.isModule) 66 indentation = '\n' ~ indentation; 67 return ImportModification("", [ 68 CodeReplacement([ 69 reader.outerImportLocation, reader.outerImportLocation 70 ], indentation ~ "import " ~ importName ~ ";" ~ (reader.outerImportLocation == 0 71 ? "\n" : "")) 72 ]); 73 } 74 else 75 { 76 indentation = (cast(ubyte[]) code).getIndentation(reader.innermostBlockStart); 77 if (reader.isModule) 78 indentation = '\n' ~ indentation; 79 return ImportModification("", [ 80 CodeReplacement([ 81 reader.innermostBlockStart, reader.innermostBlockStart 82 ], indentation ~ "import " ~ importName ~ ";") 83 ]); 84 } 85 } 86 87 /// Sorts the imports in a whitespace separated group of code 88 /// Returns `ImportBlock.init` if no changes would be done. 89 ImportBlock sortImports(scope const(char)[] code, int pos) 90 { 91 bool startBlock = true; 92 string indentation; 93 size_t start, end; 94 // find block of code separated by empty lines 95 foreach (line; code.lineSplitter!(KeepTerminator.yes)) 96 { 97 if (startBlock) 98 start = end; 99 startBlock = line.strip.length == 0; 100 if (startBlock && end >= pos) 101 break; 102 end += line.length; 103 } 104 if (start >= end || end > code.length) 105 return ImportBlock.init; 106 auto part = code[start .. end]; 107 108 // then filter out the proper indentation 109 bool inCorrectIndentationBlock; 110 size_t acc; 111 bool midImport; 112 foreach (line; part.lineSplitter!(KeepTerminator.yes)) 113 { 114 const indent = line.determineIndentation; 115 bool marksNewRegion; 116 bool leavingMidImport; 117 118 auto importStart = line.indexOfKeyword("import"); 119 const importEnd = line.indexOf(';'); 120 if (importStart != -1) 121 { 122 while (true) 123 { 124 auto rest = line[0 .. importStart].stripRight; 125 if (!rest.endsWithKeyword("public") && !rest.endsWithKeyword("static")) 126 break; 127 128 // both public and static end with c, so search for c 129 // do this to remove whitespaces 130 importStart = line[0 .. importStart].lastIndexOf('c'); 131 // both public and static have same length so subtract by "publi".length (without c) 132 importStart -= 5; 133 } 134 135 acc += importStart; 136 line = line[importStart .. $]; 137 138 if (importEnd == -1) 139 midImport = true; 140 else 141 midImport = importEnd < importStart; 142 } 143 else if (importEnd != -1 && midImport) 144 leavingMidImport = true; 145 else if (!midImport) 146 { 147 // got no "import" and wasn't in an import here 148 marksNewRegion = true; 149 } 150 151 if ((marksNewRegion || indent != indentation) && !midImport) 152 { 153 if (inCorrectIndentationBlock) 154 { 155 end = start + acc - line.stripLineEndingLength; 156 break; 157 } 158 start += acc; 159 acc = 0; 160 indentation = indent; 161 } 162 163 if (leavingMidImport) 164 midImport = false; 165 166 if (start + acc <= pos && start + acc + line.length - 1 >= pos) 167 inCorrectIndentationBlock = true; 168 acc += line.length; 169 } 170 171 part = code[start .. end]; 172 173 auto tokens = getTokensForParser(cast(ubyte[]) part, config, &workspaced.stringCache); 174 auto mod = parseModule(tokens, "code", &rba); 175 auto reader = new ImporterReaderVisitor(-1); 176 reader.visit(mod); 177 178 auto imports = reader.imports; 179 if (!imports.length) 180 return ImportBlock.init; 181 182 foreach (ref imp; imports) 183 imp.start += start; 184 185 start = imports.front.start; 186 end = code.indexOf(';', imports.back.start) + 1; 187 188 auto sorted = imports.map!(a => ImportInfo(a.name, a.rename, 189 a.selectives.dup.sort!((c, d) => sicmp(c.effectiveName, 190 d.effectiveName) < 0).array, a.isPublic, a.isStatic, a.start)).array; 191 sorted.sort!((a, b) => ImportInfo.cmp(a, b) < 0); 192 if (sorted == imports) 193 return ImportBlock.init; 194 return ImportBlock(cast(int) start, cast(int) end, sorted, indentation); 195 } 196 197 private: 198 RollbackAllocator rba; 199 LexerConfig config; 200 } 201 202 unittest 203 { 204 import std.conv : to; 205 206 void assertEqual(ImportBlock a, ImportBlock b) 207 { 208 assert(a.sameEffectAs(b), a.to!string ~ " is not equal to " ~ b.to!string); 209 } 210 211 scope backend = new WorkspaceD(); 212 auto workspace = makeTemporaryTestingWorkspace; 213 auto instance = backend.addInstance(workspace.directory); 214 backend.register!ImporterComponent; 215 216 string code = `import std.stdio; 217 import std.algorithm; 218 import std.array; 219 import std.experimental.logger; 220 import std.regex; 221 import std.functional; 222 import std.file; 223 import std.path; 224 225 import core.thread; 226 import core.sync.mutex; 227 228 import gtk.HBox, gtk.VBox, gtk.MainWindow, gtk.Widget, gtk.Button, gtk.Frame, 229 gtk.ButtonBox, gtk.Notebook, gtk.CssProvider, gtk.StyleContext, gtk.Main, 230 gdk.Screen, gtk.CheckButton, gtk.MessageDialog, gtk.Window, gtkc.gtk, 231 gtk.Label, gdk.Event; 232 233 import already; 234 import sorted; 235 236 import std.stdio : writeln, File, stdout, err = stderr; 237 238 version(unittest) 239 import std.traits; 240 import std.stdio; 241 import std.algorithm; 242 243 void main() 244 { 245 import std.stdio; 246 import std.algorithm; 247 248 writeln("foo"); 249 } 250 251 void main() 252 { 253 import std.stdio; 254 import std.algorithm; 255 } 256 257 void main() 258 { 259 import std.stdio; 260 import std.algorithm; 261 string midImport; 262 import std.string; 263 import std.array; 264 } 265 266 import workspaced.api; 267 import workspaced.helpers : determineIndentation, stripLineEndingLength, indexOfKeyword; 268 269 public import std.string; 270 public import std.stdio; 271 import std.traits; 272 import std.algorithm; 273 `; 274 275 //dfmt off 276 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 0), ImportBlock(0, 164, [ 277 ImportInfo(["std", "algorithm"]), 278 ImportInfo(["std", "array"]), 279 ImportInfo(["std", "experimental", "logger"]), 280 ImportInfo(["std", "file"]), 281 ImportInfo(["std", "functional"]), 282 ImportInfo(["std", "path"]), 283 ImportInfo(["std", "regex"]), 284 ImportInfo(["std", "stdio"]) 285 ])); 286 287 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 192), ImportBlock(166, 209, [ 288 ImportInfo(["core", "sync", "mutex"]), 289 ImportInfo(["core", "thread"]) 290 ])); 291 292 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 238), ImportBlock(211, 457, [ 293 ImportInfo(["gdk", "Event"]), 294 ImportInfo(["gdk", "Screen"]), 295 ImportInfo(["gtk", "Button"]), 296 ImportInfo(["gtk", "ButtonBox"]), 297 ImportInfo(["gtk", "CheckButton"]), 298 ImportInfo(["gtk", "CssProvider"]), 299 ImportInfo(["gtk", "Frame"]), 300 ImportInfo(["gtk", "HBox"]), 301 ImportInfo(["gtk", "Label"]), 302 ImportInfo(["gtk", "Main"]), 303 ImportInfo(["gtk", "MainWindow"]), 304 ImportInfo(["gtk", "MessageDialog"]), 305 ImportInfo(["gtk", "Notebook"]), 306 ImportInfo(["gtk", "StyleContext"]), 307 ImportInfo(["gtk", "VBox"]), 308 ImportInfo(["gtk", "Widget"]), 309 ImportInfo(["gtk", "Window"]), 310 ImportInfo(["gtkc", "gtk"]) 311 ])); 312 313 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 467), ImportBlock.init); 314 315 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 546), ImportBlock(491, 546, [ 316 ImportInfo(["std", "stdio"], "", [ 317 SelectiveImport("stderr", "err"), 318 SelectiveImport("File"), 319 SelectiveImport("stdout"), 320 SelectiveImport("writeln"), 321 ]) 322 ])); 323 324 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 593), ImportBlock(586, 625, [ 325 ImportInfo(["std", "algorithm"]), 326 ImportInfo(["std", "stdio"]) 327 ])); 328 329 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 650), ImportBlock(642, 682, [ 330 ImportInfo(["std", "algorithm"]), 331 ImportInfo(["std", "stdio"]) 332 ], "\t")); 333 334 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 730), ImportBlock(719, 759, [ 335 ImportInfo(["std", "algorithm"]), 336 ImportInfo(["std", "stdio"]) 337 ], "\t")); 338 339 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 850), ImportBlock(839, 876, [ 340 ImportInfo(["std", "array"]), 341 ImportInfo(["std", "string"]) 342 ], "\t")); 343 344 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 897), ImportBlock(880, 991, [ 345 ImportInfo(["workspaced", "api"]), 346 ImportInfo(["workspaced", "helpers"], "", [ 347 SelectiveImport("determineIndentation"), 348 SelectiveImport("indexOfKeyword"), 349 SelectiveImport("stripLineEndingLength") 350 ]) 351 ])); 352 353 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 1010), ImportBlock(993, 1084, [ 354 ImportInfo(["std", "stdio"], null, null, true), 355 ImportInfo(["std", "string"], null, null, true), 356 ImportInfo(["std", "algorithm"]), 357 ImportInfo(["std", "traits"]) 358 ])); 359 //dfmt on 360 } 361 362 /// Information about how to add an import 363 struct ImportModification 364 { 365 /// Set if there was already an import which was renamed. (for example import io = std.stdio; would be "io") 366 string rename; 367 /// Array of replacements to add the import to the code 368 CodeReplacement[] replacements; 369 } 370 371 /// Name and (if specified) rename of a symbol 372 struct SelectiveImport 373 { 374 /// Original name (always available) 375 string name; 376 /// Rename if specified 377 string rename; 378 379 /// Returns rename if set, otherwise name 380 string effectiveName() const 381 { 382 return rename.length ? rename : name; 383 } 384 385 /// Returns a D source code part 386 string toString() const 387 { 388 return (rename.length ? rename ~ " = " : "") ~ name; 389 } 390 } 391 392 /// Information about one import statement 393 struct ImportInfo 394 { 395 /// Parts of the imported module. (std.stdio -> ["std", "stdio"]) 396 string[] name; 397 /// Available if the module has been imported renamed 398 string rename; 399 /// Array of selective imports or empty if the entire module has been imported 400 SelectiveImport[] selectives; 401 /// If this is an explicitly `public import` (not checking potential attributes spanning this) 402 bool isPublic; 403 /// If this is an explicityl `static import` (not checking potential attributes spanning this) 404 bool isStatic; 405 /// Index where the first token of the import declaration starts, possibly including attributes. 406 size_t start; 407 408 /// Returns the rename if available, otherwise the name joined with dots 409 string effectiveName() const 410 { 411 return rename.length ? rename : name.join('.'); 412 } 413 414 /// Returns D source code for this import 415 string toString() const 416 { 417 import std.conv : to; 418 419 auto ret = appender!string; 420 if (isPublic) 421 ret.put("public "); 422 if (isStatic) 423 ret.put("static "); 424 ret.put("import "); 425 if (rename.length) 426 ret.put(rename ~ " = "); 427 ret.put(name.join('.')); 428 if (selectives.length) 429 ret.put(" : " ~ selectives.to!(string[]).join(", ")); 430 ret.put(';'); 431 return ret.data; 432 } 433 434 /// Returns: true if this ImportInfo is the same as another one except for definition location 435 bool sameEffectAs(in ImportInfo other) const 436 { 437 return name == other.name && rename == other.rename && selectives == other.selectives 438 && isPublic == other.isPublic && isStatic == other.isStatic; 439 } 440 441 static int cmp(ImportInfo a, ImportInfo b) 442 { 443 const ax = (a.isPublic ? 2 : 0) | (a.isStatic ? 1 : 0); 444 const bx = (b.isPublic ? 2 : 0) | (b.isStatic ? 1 : 0); 445 const x = ax - bx; 446 if (x != 0) 447 return -x; 448 449 return sicmp(a.effectiveName, b.effectiveName); 450 } 451 } 452 453 /// A block of imports generated by the sort-imports command 454 struct ImportBlock 455 { 456 /// Start & end byte index 457 int start, end; 458 /// 459 ImportInfo[] imports; 460 /// 461 string indentation; 462 463 bool sameEffectAs(in ImportBlock other) const 464 { 465 if (!(start == other.start && end == other.end && indentation == other.indentation)) 466 return false; 467 468 if (imports.length != other.imports.length) 469 return false; 470 471 foreach (i; 0 .. imports.length) 472 if (!imports[i].sameEffectAs(other.imports[i])) 473 return false; 474 475 return true; 476 } 477 } 478 479 private: 480 481 string getIndentation(ubyte[] code, size_t index) 482 { 483 import std.ascii : isWhite; 484 485 bool atLineEnd = false; 486 if (index < code.length && code[index] == '\n') 487 { 488 for (size_t i = index; i < code.length; i++) 489 if (!code[i].isWhite) 490 break; 491 atLineEnd = true; 492 } 493 while (index > 0) 494 { 495 if (code[index - 1] == cast(ubyte) '\n') 496 break; 497 index--; 498 } 499 size_t end = index; 500 while (end < code.length) 501 { 502 if (!code[end].isWhite) 503 break; 504 end++; 505 } 506 auto indent = cast(string) code[index .. end]; 507 if (!indent.length && index == 0 && !atLineEnd) 508 return " "; 509 return "\n" ~ indent.stripLeft('\n'); 510 } 511 512 unittest 513 { 514 auto code = cast(ubyte[]) "void foo() {\n\tfoo();\n}"; 515 auto indent = getIndentation(code, 20); 516 assert(indent == "\n\t", '"' ~ indent ~ '"'); 517 518 code = cast(ubyte[]) "void foo() { foo(); }"; 519 indent = getIndentation(code, 19); 520 assert(indent == " ", '"' ~ indent ~ '"'); 521 522 code = cast(ubyte[]) "import a;\n\nvoid foo() {\n\tfoo();\n}"; 523 indent = getIndentation(code, 9); 524 assert(indent == "\n", '"' ~ indent ~ '"'); 525 } 526 527 class ImporterReaderVisitor : ASTVisitor 528 { 529 this(int pos) 530 { 531 this.pos = pos; 532 inBlock = false; 533 } 534 535 alias visit = ASTVisitor.visit; 536 537 override void visit(const ModuleDeclaration decl) 538 { 539 if (pos != -1 && (decl.endLocation + 1 < outerImportLocation || inBlock)) 540 return; 541 isModule = true; 542 outerImportLocation = decl.endLocation + 1; 543 } 544 545 override void visit(const ImportDeclaration decl) 546 { 547 if (pos != -1 && decl.startIndex >= pos) 548 return; 549 isModule = false; 550 if (inBlock) 551 innermostBlockStart = decl.endIndex; 552 else 553 outerImportLocation = decl.endIndex; 554 foreach (i; decl.singleImports) 555 imports ~= ImportInfo(i.identifierChain.identifiers.map!(tok => tok.text.idup) 556 .array, i.rename.text, null, publicStack > 0, staticStack > 0, declStart); 557 if (decl.importBindings) 558 { 559 ImportInfo info; 560 if (!decl.importBindings.singleImport) 561 return; 562 info.name = decl.importBindings.singleImport.identifierChain.identifiers.map!( 563 tok => tok.text.idup).array; 564 info.rename = decl.importBindings.singleImport.rename.text; 565 foreach (bind; decl.importBindings.importBinds) 566 { 567 if (bind.right.text) 568 info.selectives ~= SelectiveImport(bind.right.text, bind.left.text); 569 else 570 info.selectives ~= SelectiveImport(bind.left.text); 571 } 572 info.start = declStart; 573 info.isPublic = publicStack > 0; 574 info.isStatic = staticStack > 0; 575 if (info.selectives.length) 576 imports ~= info; 577 } 578 } 579 580 override void visit(const Declaration decl) 581 { 582 if (decl) 583 { 584 bool hasPublic, hasStatic; 585 foreach (attr; decl.attributes) 586 { 587 if (attr.attribute == tok!"public") 588 hasPublic = true; 589 else if (attr.attribute == tok!"static") 590 hasStatic = true; 591 } 592 if (hasPublic) 593 publicStack++; 594 if (hasStatic) 595 staticStack++; 596 declStart = decl.tokens[0].index; 597 598 scope (exit) 599 { 600 if (hasStatic) 601 staticStack--; 602 if (hasPublic) 603 publicStack--; 604 declStart = -1; 605 } 606 return decl.accept(this); 607 } 608 } 609 610 override void visit(const BlockStatement content) 611 { 612 if (pos == -1 || (content && pos >= content.startLocation && pos < content.endLocation)) 613 { 614 if (content.startLocation + 1 >= innermostBlockStart) 615 innermostBlockStart = content.startLocation + 1; 616 inBlock = true; 617 return content.accept(this); 618 } 619 } 620 621 private int pos; 622 private bool inBlock; 623 private int publicStack, staticStack; 624 private size_t declStart; 625 626 ImportInfo[] imports; 627 bool isModule; 628 size_t outerImportLocation; 629 size_t innermostBlockStart; 630 } 631 632 unittest 633 { 634 import std.conv; 635 636 scope backend = new WorkspaceD(); 637 auto workspace = makeTemporaryTestingWorkspace; 638 auto instance = backend.addInstance(workspace.directory); 639 backend.register!ImporterComponent; 640 auto imports = backend.get!ImporterComponent(workspace.directory).get("import std.stdio; void foo() { import fs = std.file; import std.algorithm : map, each2 = each; writeln(\"hi\"); } void bar() { import std.string; import std.regex : ctRegex; }", 641 81); 642 bool equalsImport(ImportInfo i, string s) 643 { 644 return i.name.join('.') == s; 645 } 646 647 void assertEquals(T)(T a, T b) 648 { 649 assert(a == b, "'" ~ a.to!string ~ "' != '" ~ b.to!string ~ "'"); 650 } 651 652 assertEquals(imports.length, 3); 653 assert(equalsImport(imports[0], "std.stdio")); 654 assert(equalsImport(imports[1], "std.file")); 655 assertEquals(imports[1].rename, "fs"); 656 assert(equalsImport(imports[2], "std.algorithm")); 657 assertEquals(imports[2].selectives.length, 2); 658 assertEquals(imports[2].selectives[0].name, "map"); 659 assertEquals(imports[2].selectives[1].name, "each"); 660 assertEquals(imports[2].selectives[1].rename, "each2"); 661 662 string code = "void foo() { import std.stdio : stderr; writeln(\"hi\"); }"; 663 auto mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 664 assertEquals(mod.rename, ""); 665 assertEquals(mod.replacements.length, 1); 666 assertEquals(mod.replacements[0].apply(code), 667 "void foo() { import std.stdio : stderr; import std.stdio; writeln(\"hi\"); }"); 668 669 code = "void foo() {\n\timport std.stdio : stderr;\n\twriteln(\"hi\");\n}"; 670 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 671 assertEquals(mod.rename, ""); 672 assertEquals(mod.replacements.length, 1); 673 assertEquals(mod.replacements[0].apply(code), 674 "void foo() {\n\timport std.stdio : stderr;\n\timport std.stdio;\n\twriteln(\"hi\");\n}"); 675 676 code = "void foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}"; 677 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 678 assertEquals(mod.rename, ""); 679 assertEquals(mod.replacements.length, 1); 680 assertEquals(mod.replacements[0].apply(code), 681 "import std.stdio;\nvoid foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}"); 682 683 code = "void foo() { import io = std.stdio; io.writeln(\"hi\"); }"; 684 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 685 assertEquals(mod.rename, "io"); 686 assertEquals(mod.replacements.length, 0); 687 688 code = "import std.file : readText;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 689 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 690 assertEquals(mod.rename, ""); 691 assertEquals(mod.replacements.length, 1); 692 assertEquals(mod.replacements[0].apply(code), 693 "import std.file : readText;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 694 695 code = "import std.file;\nimport std.regex;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 696 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 54); 697 assertEquals(mod.rename, ""); 698 assertEquals(mod.replacements.length, 1); 699 assertEquals(mod.replacements[0].apply(code), 700 "import std.file;\nimport std.regex;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 701 702 code = "module a;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 703 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 30); 704 assertEquals(mod.rename, ""); 705 assertEquals(mod.replacements.length, 1); 706 assertEquals(mod.replacements[0].apply(code), 707 "module a;\n\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 708 }