Skip to content

Commit f00c190

Browse files
committed
Smarter jass validation
1 parent e640e98 commit f00c190

2 files changed

Lines changed: 424 additions & 6 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/ModelManagerImpl.java

Lines changed: 290 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.common.base.Charsets;
44
import com.google.common.collect.*;
5+
import com.google.common.io.BaseEncoding;
56
import com.google.common.io.Files;
67
import de.peeeq.wurstio.ModelChangedException;
78
import de.peeeq.wurstio.WurstCompilerJassImpl;
@@ -10,6 +11,8 @@
1011
import de.peeeq.wurstscript.WLogger;
1112
import de.peeeq.wurstscript.ast.*;
1213
import de.peeeq.wurstscript.attributes.CompileError;
14+
import de.peeeq.wurstscript.attributes.prettyPrint.DefaultSpacer;
15+
import de.peeeq.wurstscript.attributes.prettyPrint.PrettyPrinter;
1316
import de.peeeq.wurstscript.gui.WurstGui;
1417
import de.peeeq.wurstscript.gui.WurstGuiLogger;
1518
import de.peeeq.wurstscript.utils.Utils;
@@ -18,6 +21,8 @@
1821
import org.eclipse.lsp4j.PublishDiagnosticsParams;
1922

2023
import java.io.*;
24+
import java.security.MessageDigest;
25+
import java.security.NoSuchAlgorithmException;
2126
import java.nio.file.Path;
2227
import java.nio.file.StandardCopyOption;
2328
import java.util.*;
@@ -47,6 +52,12 @@ public class ModelManagerImpl implements ModelManager {
4752
// hashcode for each compilation unit content as string
4853
private final Map<WFile, Integer> fileHashcodes = new HashMap<>();
4954

55+
// hash for each function inside a Jass compilation unit
56+
private final Map<WFile, Map<String, String>> jassFunctionSnapshots = new HashMap<>();
57+
58+
// functions that changed in recently modified Jass compilation units
59+
private final Map<WFile, Set<String>> pendingJassFunctionChanges = new HashMap<>();
60+
5061
// file for each compilation unit
5162
private final WeakHashMap<CompilationUnit, WFile> compilationunitFile = new WeakHashMap<>();
5263

@@ -88,6 +99,10 @@ private List<CompilationUnit> getJassdocCUs(Path jassdoc, WurstGui gui) {
8899
@Override
89100
public Changes removeCompilationUnit(WFile resource) {
90101
parseErrors.remove(resource);
102+
if (isJassFile(resource)) {
103+
jassFunctionSnapshots.remove(resource);
104+
pendingJassFunctionChanges.remove(resource);
105+
}
91106
WurstModel model2 = model;
92107
if (model2 == null) {
93108
return Changes.empty();
@@ -114,6 +129,8 @@ public void clean() {
114129
parseErrors.clear();
115130
model = null;
116131
dependencies.clear();
132+
jassFunctionSnapshots.clear();
133+
pendingJassFunctionChanges.clear();
117134
WLogger.info("Clean done.");
118135
}
119136

@@ -539,6 +556,17 @@ private CompilationUnit replaceCompilationUnit(WFile filename, String contents,
539556
WurstCompilerJassImpl c = getCompiler(gui);
540557
CompilationUnit cu = c.parse(filename.toString(), new StringReader(contents));
541558
cu.getCuInfo().setFile(filename.toString());
559+
560+
if (isJassFile(filename)) {
561+
Map<String, String> newFunctions = collectJassFunctions(cu);
562+
Map<String, String> oldFunctions = jassFunctionSnapshots.getOrDefault(filename, Collections.emptyMap());
563+
Set<String> changedFunctions = determineChangedJassFunctions(oldFunctions, newFunctions);
564+
pendingJassFunctionChanges.put(filename, changedFunctions);
565+
jassFunctionSnapshots.put(filename, newFunctions);
566+
} else {
567+
pendingJassFunctionChanges.remove(filename);
568+
}
569+
542570
updateModel(cu, gui);
543571
fileHashcodes.put(filename, contents.hashCode());
544572
if (reportErrors) {
@@ -625,6 +653,7 @@ private void doTypeCheckPartial(WurstGui gui, List<WFile> toCheckFilenames, Set<
625653
Collection<CompilationUnit> toCheckRec = calculateCUsToUpdate(toCheck, oldPackages, model2);
626654

627655
partialTypecheck(model2, toCheckRec, gui, comp);
656+
clearPendingJassChanges(toCheckFilenames);
628657
}
629658

630659
@Override
@@ -644,6 +673,7 @@ public void reconcile(Changes changes) {
644673
WurstGui gui = new WurstGuiLogger();
645674
WurstCompilerJassImpl comp = getCompiler(gui);
646675
partialTypecheck(model2, toCheckRec, gui, comp);
676+
clearPendingJassChanges(changes.getAffectedFiles().toJavaSet());
647677
}
648678

649679
private void partialTypecheck(WurstModel model2, Collection<CompilationUnit> toCheckRec, WurstGui gui, WurstCompilerJassImpl comp) {
@@ -676,18 +706,26 @@ private Set<CompilationUnit> calculateCUsToUpdate(Collection<CompilationUnit> ch
676706
Set<CompilationUnit> result = new TreeSet<>(Comparator.comparing(cu -> cu.getCuInfo().getFile()));
677707
result.addAll(changed);
678708

679-
boolean b = false;
709+
Map<WFile, Set<String>> changedJassFunctions = new HashMap<>();
710+
boolean missingJassInfo = false;
680711
for (CompilationUnit compilationUnit : changed) {
681-
if (compilationUnit.getCuInfo().getFile().endsWith(".j")) {
682-
b = true;
683-
break;
712+
if (isJassFile(compilationUnit)) {
713+
WFile file = wFile(compilationUnit);
714+
Set<String> changedFunctions = pendingJassFunctionChanges.get(file);
715+
if (changedFunctions == null) {
716+
missingJassInfo = true;
717+
} else {
718+
changedJassFunctions.put(file, changedFunctions);
719+
}
684720
}
685721
}
686-
if (b) {
687-
// when plain Jass files are changed, everything must be checked again:
722+
if (missingJassInfo) {
688723
result.addAll(model);
689724
return result;
690725
}
726+
if (!changedJassFunctions.isEmpty()) {
727+
addAffectedByJass(changedJassFunctions, model, result);
728+
}
691729

692730
// get packages provided by the changed CUs
693731
Stream<String> providedPackages = changed.stream()
@@ -703,6 +741,236 @@ private Set<CompilationUnit> calculateCUsToUpdate(Collection<CompilationUnit> ch
703741
return result;
704742
}
705743

744+
private Map<String, String> collectJassFunctions(CompilationUnit cu) {
745+
Map<String, String> result = new LinkedHashMap<>();
746+
for (JassToplevelDeclaration decl : cu.getJassDecls()) {
747+
if (decl instanceof FunctionDefinition) {
748+
FunctionDefinition function = (FunctionDefinition) decl;
749+
result.put(function.getName(), fingerprintJassFunction(function));
750+
}
751+
}
752+
return result;
753+
}
754+
755+
private String fingerprintJassFunction(FunctionDefinition function) {
756+
DefaultSpacer spacer = new DefaultSpacer();
757+
String rendered = function.match(new FunctionDefinition.Matcher<String>() {
758+
@Override
759+
public String case_NativeFunc(NativeFunc nativeFunc) {
760+
return prettyPrint(nativeFunc, spacer);
761+
}
762+
763+
@Override
764+
public String case_TupleDef(TupleDef tupleDef) {
765+
return prettyPrint(tupleDef, spacer);
766+
}
767+
768+
@Override
769+
public String case_ExtensionFuncDef(ExtensionFuncDef extensionFuncDef) {
770+
return prettyPrint(extensionFuncDef, spacer);
771+
}
772+
773+
@Override
774+
public String case_FuncDef(FuncDef funcDef) {
775+
return prettyPrint(funcDef, spacer);
776+
}
777+
});
778+
return sha256(rendered);
779+
}
780+
781+
private String prettyPrint(NativeFunc nativeFunc, DefaultSpacer spacer) {
782+
StringBuilder sb = new StringBuilder();
783+
seedBuilder(sb);
784+
PrettyPrinter.jassPrettyPrint(nativeFunc, spacer, sb, 0);
785+
trimBuilderPrefix(sb);
786+
return sb.toString();
787+
}
788+
789+
private String prettyPrint(FuncDef funcDef, DefaultSpacer spacer) {
790+
StringBuilder sb = new StringBuilder();
791+
seedBuilder(sb);
792+
PrettyPrinter.jassPrettyPrint(funcDef, spacer, sb, 0);
793+
trimBuilderPrefix(sb);
794+
return sb.toString();
795+
}
796+
797+
private String prettyPrint(FunctionDefinition function, DefaultSpacer spacer) {
798+
StringBuilder sb = new StringBuilder();
799+
seedBuilder(sb);
800+
function.prettyPrint(spacer, sb, 0);
801+
trimBuilderPrefix(sb);
802+
return sb.toString();
803+
}
804+
805+
private void seedBuilder(StringBuilder sb) {
806+
sb.append('\n').append('\n');
807+
}
808+
809+
private void trimBuilderPrefix(StringBuilder sb) {
810+
while (sb.length() > 0 && sb.charAt(0) == '\n') {
811+
sb.deleteCharAt(0);
812+
}
813+
}
814+
815+
private String sha256(String content) {
816+
try {
817+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
818+
byte[] hash = digest.digest(content.getBytes(UTF_8));
819+
return BaseEncoding.base16().lowerCase().encode(hash);
820+
} catch (NoSuchAlgorithmException e) {
821+
throw new IllegalStateException("SHA-256 not available", e);
822+
}
823+
}
824+
825+
private Set<String> determineChangedJassFunctions(Map<String, String> oldFunctions, Map<String, String> newFunctions) {
826+
Set<String> changed = new HashSet<>();
827+
for (Map.Entry<String, String> entry : newFunctions.entrySet()) {
828+
String oldFingerprint = oldFunctions.get(entry.getKey());
829+
String newFingerprint = entry.getValue();
830+
if (oldFingerprint == null || !newFingerprint.equals(oldFingerprint)) {
831+
changed.add(entry.getKey());
832+
}
833+
}
834+
for (String oldName : oldFunctions.keySet()) {
835+
if (!newFunctions.containsKey(oldName)) {
836+
changed.add(oldName);
837+
}
838+
}
839+
return Collections.unmodifiableSet(changed);
840+
}
841+
842+
private void addAffectedByJass(Map<WFile, Set<String>> changedJassFunctions, WurstModel model, Set<CompilationUnit> result) {
843+
boolean hasRealChanges = changedJassFunctions.values().stream().anyMatch(s -> !s.isEmpty());
844+
if (!hasRealChanges) {
845+
return;
846+
}
847+
for (CompilationUnit cu : model) {
848+
if (result.contains(cu)) {
849+
continue;
850+
}
851+
if (usesChangedJassFunctions(cu, changedJassFunctions)) {
852+
result.add(cu);
853+
}
854+
}
855+
}
856+
857+
private boolean usesChangedJassFunctions(CompilationUnit cu, Map<WFile, Set<String>> changedJassFunctions) {
858+
JassFunctionUsageCollector collector = new JassFunctionUsageCollector(changedJassFunctions);
859+
cu.accept(collector);
860+
return collector.isFound();
861+
}
862+
863+
private final class JassFunctionUsageCollector extends Element.DefaultVisitor {
864+
private final Map<WFile, Set<String>> changedJassFunctions;
865+
private boolean found = false;
866+
867+
private JassFunctionUsageCollector(Map<WFile, Set<String>> changedJassFunctions) {
868+
this.changedJassFunctions = changedJassFunctions;
869+
}
870+
871+
private boolean isFound() {
872+
return found;
873+
}
874+
875+
private void checkFuncRef(FuncRef funcRef) {
876+
if (found) {
877+
return;
878+
}
879+
FunctionDefinition funcDef = null;
880+
try {
881+
funcDef = funcRef.attrFuncDef();
882+
} catch (RuntimeException ignored) {
883+
// fall back to name-based matching below
884+
}
885+
if (funcDef != null) {
886+
CompilationUnit defCu = funcDef.attrCompilationUnit();
887+
if (defCu == null) {
888+
return;
889+
}
890+
WFile defFile = wFile(defCu);
891+
Set<String> changed = changedJassFunctions.get(defFile);
892+
if (changed == null || changed.isEmpty()) {
893+
return;
894+
}
895+
if (changed.contains(funcDef.getName())) {
896+
found = true;
897+
}
898+
return;
899+
}
900+
String funcName = funcRef.getFuncName();
901+
if (functionNameChanged(funcName)) {
902+
found = true;
903+
}
904+
}
905+
906+
private boolean functionNameChanged(String funcName) {
907+
if (funcName == null) {
908+
return false;
909+
}
910+
for (Set<String> changed : changedJassFunctions.values()) {
911+
if (changed.contains(funcName)) {
912+
return true;
913+
}
914+
}
915+
return false;
916+
}
917+
918+
@Override
919+
public void visit(ExprFunctionCall e) {
920+
if (found) {
921+
return;
922+
}
923+
checkFuncRef(e);
924+
if (!found) {
925+
super.visit(e);
926+
}
927+
}
928+
929+
@Override
930+
public void visit(ExprMemberMethodDot e) {
931+
if (found) {
932+
return;
933+
}
934+
checkFuncRef(e);
935+
if (!found) {
936+
super.visit(e);
937+
}
938+
}
939+
940+
@Override
941+
public void visit(ExprMemberMethodDotDot e) {
942+
if (found) {
943+
return;
944+
}
945+
checkFuncRef(e);
946+
if (!found) {
947+
super.visit(e);
948+
}
949+
}
950+
951+
@Override
952+
public void visit(ExprFuncRef e) {
953+
if (found) {
954+
return;
955+
}
956+
checkFuncRef(e);
957+
if (!found) {
958+
super.visit(e);
959+
}
960+
}
961+
962+
@Override
963+
public void visit(Annotation annotation) {
964+
if (found) {
965+
return;
966+
}
967+
checkFuncRef(annotation);
968+
if (!found) {
969+
super.visit(annotation);
970+
}
971+
}
972+
}
973+
706974

707975
/**
708976
* Add all packages that directly or indirectly depend on the providedPackages
@@ -792,6 +1060,22 @@ private void addDependencyWurstFiles(Set<File> result, File file) {
7921060
}
7931061
}
7941062

1063+
private boolean isJassFile(CompilationUnit cu) {
1064+
return cu.getCuInfo().getFile().endsWith(".j");
1065+
}
1066+
1067+
private boolean isJassFile(WFile file) {
1068+
return file.toString().endsWith(".j");
1069+
}
1070+
1071+
private void clearPendingJassChanges(Collection<WFile> files) {
1072+
for (WFile file : files) {
1073+
if (isJassFile(file)) {
1074+
pendingJassFunctionChanges.remove(file);
1075+
}
1076+
}
1077+
}
1078+
7951079
private WFile wFile(CompilationUnit cu) {
7961080
return compilationunitFile.computeIfAbsent(cu, c -> WFile.create(cu.getCuInfo().getFile()));
7971081
}

0 commit comments

Comments
 (0)