|
1 | 1 | package tests.wurstscript.tests; |
2 | 2 |
|
3 | 3 | import com.google.common.collect.ImmutableSet; |
| 4 | +import de.peeeq.wurstio.TimeTaker; |
| 5 | +import de.peeeq.wurstio.WurstCompilerJassImpl; |
4 | 6 | import de.peeeq.wurstio.languageserver.BufferManager; |
5 | 7 | import de.peeeq.wurstio.languageserver.ModelManager; |
6 | 8 | import de.peeeq.wurstio.languageserver.ModelManagerImpl; |
7 | 9 | import de.peeeq.wurstio.languageserver.WFile; |
8 | 10 | import de.peeeq.wurstio.utils.FileUtils; |
| 11 | +import de.peeeq.wurstscript.RunArgs; |
9 | 12 | import de.peeeq.wurstscript.ast.*; |
| 13 | +import de.peeeq.wurstscript.gui.WurstGui; |
| 14 | +import de.peeeq.wurstscript.gui.WurstGuiLogger; |
| 15 | +import de.peeeq.wurstscript.types.WurstType; |
| 16 | +import de.peeeq.wurstscript.types.WurstTypeClass; |
| 17 | +import de.peeeq.wurstscript.types.WurstTypeString; |
10 | 18 | import de.peeeq.wurstscript.utils.Utils; |
| 19 | +import de.peeeq.wurstscript.validation.GlobalCaches; |
11 | 20 | import org.eclipse.lsp4j.Diagnostic; |
12 | 21 | import org.eclipse.lsp4j.PublishDiagnosticsParams; |
13 | 22 | import org.hamcrest.CoreMatchers; |
|
20 | 29 | import java.nio.file.Files; |
21 | 30 | import java.util.HashMap; |
22 | 31 | import java.util.Map; |
| 32 | +import java.util.concurrent.atomic.AtomicBoolean; |
23 | 33 | import java.util.stream.Collectors; |
24 | 34 |
|
25 | 35 | import static org.hamcrest.CoreMatchers.containsString; |
@@ -249,7 +259,7 @@ private Map<WFile, String> keepErrorsInMap(ModelManagerImpl manager) { |
249 | 259 | results.put(WFile.create(res.getUri()), errors); |
250 | 260 |
|
251 | 261 | for (Diagnostic err : res.getDiagnostics()) { |
252 | | - System.out.println(" " + err.getSeverity() + " in " + res.getUri() + ", line " + err.getRange().getStart().getLine() + "\n " + err.getMessage()); |
| 262 | + System.out.println(" " + err.getSeverity() + " in " + res.getUri() + ", line " + err.getRange().getStart().getLine() + "\n " + err.getMessage()); |
253 | 263 | } |
254 | 264 | }); |
255 | 265 | return results; |
@@ -587,5 +597,227 @@ public void keepTypeErrorsWhileEditing() throws IOException { |
587 | 597 | assertThat(errors.get(fileT1), CoreMatchers.containsString("extraneous input '(' expecting NL")); |
588 | 598 | } |
589 | 599 |
|
| 600 | + @Test |
| 601 | + public void runmapLikePipeline_cacheRegression_closerToRunMap() throws Exception { |
| 602 | + File projectFolder = new File("./temp/testProject_runmap_like_real/"); |
| 603 | + File wurstFolder = new File(projectFolder, "wurst"); |
| 604 | + newCleanFolder(wurstFolder); |
| 605 | + |
| 606 | + // --- Minimal LinkedList-style module --- |
| 607 | + String pkgLinkedList = string( |
| 608 | + "package LinkedListModule", |
| 609 | + "public module LinkedListModule", |
| 610 | + " static function iterator() returns Iterator", |
| 611 | + " return new Iterator()", |
| 612 | + " static class Iterator", |
| 613 | + " LinkedListModule.thistype current = null", |
| 614 | + " function hasNext() returns boolean", |
| 615 | + " return false", |
| 616 | + " function next() returns LinkedListModule.thistype", |
| 617 | + " return current", |
| 618 | + " function close()", |
| 619 | + " destroy this" |
| 620 | + ); |
| 621 | + |
| 622 | + // --- Minimal string iterator stub to create competing iterator symbol --- |
| 623 | + String pkgStrIter = string( |
| 624 | + "package StrIter", |
| 625 | + "public function string.iterator() returns StringIterator", |
| 626 | + " return new StringIterator(this)", |
| 627 | + "public class StringIterator", |
| 628 | + " string str", |
| 629 | + " construct(string s)", |
| 630 | + " this.str = s", |
| 631 | + " function hasNext() returns boolean", |
| 632 | + " return false", |
| 633 | + " function next() returns string", |
| 634 | + " return \"\"", |
| 635 | + " function close()", |
| 636 | + " destroy this" |
| 637 | + ); |
| 638 | + |
| 639 | + // --- Reproducer using both packages --- |
| 640 | + String pkgHello = string( |
| 641 | + "package Hello", |
| 642 | + "import LinkedListModule", |
| 643 | + "import StrIter", |
| 644 | + "", |
| 645 | + "class A", |
| 646 | + " use LinkedListModule", |
| 647 | + "", |
| 648 | + "init", |
| 649 | + " let itr = A.iterator()", |
| 650 | + " while itr.hasNext()", |
| 651 | + " let a = itr.next()", |
| 652 | + " destroy a", |
| 653 | + " itr.close()", |
| 654 | + " let s = \"hello\".iterator()", |
| 655 | + " while s.hasNext()", |
| 656 | + " BJDebugMsg(s.next())", |
| 657 | + " s.close()" |
| 658 | + ); |
| 659 | + |
| 660 | + // A tiny JASS file so purge logic and JASS transform path look more like RunMap’s environment: |
| 661 | + String jass = "globals\nendglobals\n// war3map.j placeholder"; |
| 662 | + |
| 663 | + WFile fileLinkedList = WFile.create(new File(wurstFolder, "LinkedListModule.wurst")); |
| 664 | + WFile fileStrIter = WFile.create(new File(wurstFolder, "StrIter.wurst")); |
| 665 | + WFile fileHello = WFile.create(new File(wurstFolder, "Hello.wurst")); |
| 666 | + WFile fileWurst = WFile.create(new File(wurstFolder, "Wurst.wurst")); |
| 667 | + WFile fileWar3mapJ = WFile.create(new File(wurstFolder, "war3map.j")); |
| 668 | + |
| 669 | + writeFile(fileLinkedList, pkgLinkedList); |
| 670 | + writeFile(fileStrIter, pkgStrIter); |
| 671 | + writeFile(fileHello, pkgHello); |
| 672 | + writeFile(fileWurst, "package Wurst\n"); |
| 673 | + writeFile(fileWar3mapJ, jass); |
| 674 | + |
| 675 | + ModelManagerImpl manager = new ModelManagerImpl(projectFolder, new BufferManager()); |
| 676 | + Map<WFile, String> diags = keepErrorsInMap(manager); |
| 677 | + |
| 678 | + // Baseline build (matches "no IDE errors") |
| 679 | + manager.buildProject(); |
| 680 | + assertEquals(diags.get(fileHello), "", "initial build must be clean"); |
| 681 | + |
| 682 | + // VSCode-like reconcile touch: |
| 683 | + ModelManager.Changes changes = manager.syncCompilationUnitContent(fileHello, pkgHello + "\n// touch"); |
| 684 | + manager.reconcile(changes); |
| 685 | + assertEquals(diags.get(fileHello), "", "reconcile after harmless edit must be clean"); |
| 686 | + GlobalCaches.printStats(); |
| 687 | + |
| 688 | + // --- First RunMap-like compile on SAME model --- |
| 689 | + touchWar3mapJ(manager, fileWar3mapJ, " // pass 1"); |
| 690 | + runRunmapLikeCompile_Closer(projectFolder, manager); |
| 691 | + assertLocalAIsClassA(manager.getCompilationUnit(fileHello)); |
| 692 | + |
| 693 | + GlobalCaches.printStats(); |
| 694 | + |
| 695 | + // --- Second RunMap-like compile (caches should not corrupt resolution) --- |
| 696 | + touchWar3mapJ(manager, fileWar3mapJ, " // pass 2"); |
| 697 | + runRunmapLikeCompile_Closer(projectFolder, manager); |
| 698 | + assertLocalAIsClassA(manager.getCompilationUnit(fileHello)); |
| 699 | + |
| 700 | + GlobalCaches.printStats(); |
| 701 | + } |
| 702 | + |
| 703 | + |
| 704 | + /** Simulate RunMap.replaceBaseScriptWithConfig(..): rewrite war3map.j inside the SAME model. */ |
| 705 | + private void touchWar3mapJ(ModelManagerImpl manager, WFile war3map, String suffix) throws IOException { |
| 706 | + String content = "globals\nendglobals\n// war3map.j placeholder" + suffix + "\n"; |
| 707 | + manager.syncCompilationUnitContent(war3map, content); |
| 708 | + } |
| 709 | + |
| 710 | + /** Run a RunMap-like compile on the SAME model: purge imports, check → IM, then JASS transform. */ |
| 711 | + private void runRunmapLikeCompile_Closer(File projectFolder, ModelManagerImpl manager) { |
| 712 | + WurstGui gui = new WurstGuiLogger(); |
| 713 | + TimeTaker time = new TimeTaker.Default(); |
| 714 | + WurstCompilerJassImpl compiler = |
| 715 | + new WurstCompilerJassImpl(time, projectFolder, gui, null, new RunArgs(java.util.Collections.emptyList())); |
| 716 | + |
| 717 | + WurstModel model = manager.getModel(); |
| 718 | + |
| 719 | + // 2) Check program |
| 720 | + gui.sendProgress("Check program"); |
| 721 | + compiler.checkProg(model); |
| 722 | + if (gui.getErrorCount() > 0) { |
| 723 | + throw new AssertionError("RunMap-like checkProg reported errors: " + gui.getErrorList()); |
| 724 | + } |
| 725 | + |
| 726 | + // 3) Translate to IM |
| 727 | + compiler.translateProgToIm(model); |
| 728 | + if (gui.getErrorCount() > 0) { |
| 729 | + throw new AssertionError("RunMap-like translateProgToIm reported errors: " + gui.getErrorList()); |
| 730 | + } |
| 731 | + |
| 732 | + // 4) (Optional but closer to reality) Transform to JASS |
| 733 | + compiler.transformProgToJass(); |
| 734 | + // We don't run PJass here; just exercising additional phases & caches. |
| 735 | + } |
| 736 | + |
| 737 | + /** Same as MapRequest.purgeUnimportedFiles: keep wurst-folder CUs and any imported transitively, plus .j files. */ |
| 738 | + private void purgeUnimportedFiles_likeRunMap(WurstModel model, ModelManagerImpl manager) { |
| 739 | + // Seed: files inside project root (wurst folder) OR .j files. |
| 740 | + java.util.Set<CompilationUnit> keep = model.stream() |
| 741 | + .filter(cu -> isInWurstFolder_likeRunMap(cu.getCuInfo().getFile(), manager) || cu.getCuInfo().getFile().endsWith(".j")) |
| 742 | + .collect(java.util.stream.Collectors.toSet()); |
| 743 | + |
| 744 | + // Recursively add imported packages’ CUs (uses attrImportedPackage like RunMap) |
| 745 | + addImports_likeRunMap(keep, keep); |
| 746 | + model.removeIf(cu -> !keep.contains(cu)); |
| 747 | + } |
| 748 | + |
| 749 | + private boolean isInWurstFolder_likeRunMap(String file, ModelManagerImpl manager) { |
| 750 | + java.nio.file.Path p = java.nio.file.Paths.get(file); |
| 751 | + java.nio.file.Path w = manager.getProjectPath().toPath(); // project root |
| 752 | + return p.startsWith(w) |
| 753 | + && java.nio.file.Files.exists(p) |
| 754 | + && Utils.isWurstFile(file); |
| 755 | + } |
| 756 | + |
| 757 | + private void addImports_likeRunMap(java.util.Set<CompilationUnit> result, java.util.Set<CompilationUnit> toAdd) { |
| 758 | + java.util.Set<CompilationUnit> imported = |
| 759 | + toAdd.stream() |
| 760 | + .flatMap(cu -> cu.getPackages().stream()) |
| 761 | + .flatMap(p -> p.getImports().stream()) |
| 762 | + .map(WImport::attrImportedPackage) |
| 763 | + .filter(java.util.Objects::nonNull) |
| 764 | + .map(WPackage::attrCompilationUnit) |
| 765 | + .collect(java.util.stream.Collectors.toSet()); |
| 766 | + boolean changed = result.addAll(imported); |
| 767 | + if (changed) addImports_likeRunMap(result, imported); |
| 768 | + } |
| 769 | + |
| 770 | + /** Assert that 'let a = itr.next()' has type A and that next() returns A. */ |
| 771 | + private void assertLocalAIsClassA(CompilationUnit cu) { |
| 772 | + final AtomicBoolean checked = new AtomicBoolean(false); |
| 773 | + final AtomicBoolean didFindA = new AtomicBoolean(false); |
| 774 | + final AtomicBoolean didFindString = new AtomicBoolean(false); |
| 775 | + |
| 776 | + cu.accept(new Element.DefaultVisitor() { |
| 777 | + @Override public void visit(LocalVarDef l) { |
| 778 | + if (!"a".equals(l.getNameId().getName())) { super.visit(l); return; } |
| 779 | + |
| 780 | + VarInitialization init = l.getInitialExpr(); |
| 781 | + if (init == null) throw new AssertionError("local 'a' has no initializer"); |
| 782 | + |
| 783 | + if (init instanceof ExprMemberMethod) { |
| 784 | + WurstType t = ((ExprMemberMethod) init).attrTyp(); |
| 785 | + if (t instanceof WurstTypeClass) { |
| 786 | + String cname = ((WurstTypeClass) t).getClassDef().getName(); |
| 787 | + assertEquals(cname, "A", "Expected local 'a' to be of class A"); |
| 788 | + } else { |
| 789 | + throw new AssertionError("Expected class type for 'a', but got: " + t); |
| 790 | + } |
| 791 | + } |
| 792 | + |
| 793 | + checked.set(true); |
| 794 | + super.visit(l); |
| 795 | + } |
| 796 | + |
| 797 | + @Override public void visit(ExprMemberMethodDot call) { |
| 798 | + if ("next".equals(call.getFuncName())) { |
| 799 | + WurstType rt = call.attrTyp(); |
| 800 | + if (rt instanceof WurstTypeClass) { |
| 801 | + String cname = ((WurstTypeClass) rt).getClassDef().getName(); |
| 802 | + assertEquals(cname, "A", "next() must return A"); |
| 803 | + didFindA.set(true); |
| 804 | + } else if (rt instanceof WurstTypeString) { |
| 805 | + String cname = ((WurstTypeString) rt).getName(); |
| 806 | + assertEquals(cname, "string", "next() must return string"); |
| 807 | + didFindString.set(true); |
| 808 | + } else { |
| 809 | + throw new AssertionError("next() must return class A or string, but was: " + rt); |
| 810 | + } |
| 811 | + } |
| 812 | + super.visit(call); |
| 813 | + } |
| 814 | + }); |
| 815 | + |
| 816 | + if (!checked.get()) throw new AssertionError("Did not find local var 'a' to assert."); |
| 817 | + if (!didFindA.get()) throw new AssertionError("Did not find call to next() returning class A."); |
| 818 | + if (!didFindString.get()) throw new AssertionError("Did not find call to next() returning string."); |
| 819 | + } |
| 820 | + |
| 821 | + |
590 | 822 |
|
591 | 823 | } |
0 commit comments