22
33import com .google .common .base .Charsets ;
44import com .google .common .collect .*;
5+ import com .google .common .io .BaseEncoding ;
56import com .google .common .io .Files ;
67import de .peeeq .wurstio .ModelChangedException ;
78import de .peeeq .wurstio .WurstCompilerJassImpl ;
1011import de .peeeq .wurstscript .WLogger ;
1112import de .peeeq .wurstscript .ast .*;
1213import de .peeeq .wurstscript .attributes .CompileError ;
14+ import de .peeeq .wurstscript .attributes .prettyPrint .DefaultSpacer ;
15+ import de .peeeq .wurstscript .attributes .prettyPrint .PrettyPrinter ;
1316import de .peeeq .wurstscript .gui .WurstGui ;
1417import de .peeeq .wurstscript .gui .WurstGuiLogger ;
1518import de .peeeq .wurstscript .utils .Utils ;
1821import org .eclipse .lsp4j .PublishDiagnosticsParams ;
1922
2023import java .io .*;
24+ import java .security .MessageDigest ;
25+ import java .security .NoSuchAlgorithmException ;
2126import java .nio .file .Path ;
2227import java .nio .file .StandardCopyOption ;
2328import 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