From 2aee09fb8b53072bd883087eaf7b6e926c0e363e Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Sat, 7 Apr 2018 22:52:52 +0200 Subject: [PATCH] support of printing of changes in list (e.g. type members) --- .../spoon/reflect/visitor/TokenWriter.java | 7 +- .../ChangesAwareDefaultJavaPrettyPrinter.java | 60 +++++-- .../printer/change/SourceFragment.java | 35 +++- .../printer/change/SourceFragmentContext.java | 20 ++- .../change/SourceFragmentContextList.java | 161 ++++++++++++++++++ .../change/SourceFragmentContextNormal.java | 40 +++-- .../printer/change/SourcePositionUtils.java | 16 +- 7 files changed, 298 insertions(+), 41 deletions(-) create mode 100644 src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextList.java diff --git a/src/main/java/spoon/reflect/visitor/TokenWriter.java b/src/main/java/spoon/reflect/visitor/TokenWriter.java index 67f08662d77..ec662850f1b 100644 --- a/src/main/java/spoon/reflect/visitor/TokenWriter.java +++ b/src/main/java/spoon/reflect/visitor/TokenWriter.java @@ -133,9 +133,8 @@ public interface TokenWriter { */ void reset(); - /** Writes a single space. - * - * Note that this method is only there for low-level implementation reasons. A default implementation simply calls {@link PrinterHelper#writeSpace()} ()}. This method will be removed in a later refactoring. - * */ + /** + * Writes a single space. + */ TokenWriter writeSpace(); } diff --git a/src/main/java/spoon/reflect/visitor/printer/change/ChangesAwareDefaultJavaPrettyPrinter.java b/src/main/java/spoon/reflect/visitor/printer/change/ChangesAwareDefaultJavaPrettyPrinter.java index 1bd63951f33..34b2af9c258 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/ChangesAwareDefaultJavaPrettyPrinter.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/ChangesAwareDefaultJavaPrettyPrinter.java @@ -26,6 +26,7 @@ import spoon.SpoonException; import spoon.compiler.Environment; import spoon.experimental.modelobs.ChangeCollector; +import spoon.reflect.code.CtComment; import spoon.reflect.declaration.CtElement; import spoon.reflect.path.CtRole; import spoon.reflect.visitor.DefaultJavaPrettyPrinter; @@ -65,29 +66,51 @@ private TokenWriter createTokenWriterListener(TokenWriter tokenWriter) { new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Runnable printAction = () -> { + try { + method.invoke(tokenWriter, args); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new SpoonException("Cannot invoke TokenWriter method", e); + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof RuntimeException) { + throw (RuntimeException) e.getTargetException(); + } + throw new SpoonException("Invokation target exception TokenWriter method", e); + } + }; if (method.getName().startsWith("write")) { Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length == 1 && paramTypes[0] == String.class) { - onTokenWriterWrite(method.getName(), (String) args[0], () -> { - try { - method.invoke(tokenWriter, args); - } catch (IllegalAccessException | IllegalArgumentException e) { - throw new SpoonException("Cannot invoke TokenWriter method", e); - } catch (InvocationTargetException e) { - if (e.getTargetException() instanceof RuntimeException) { - throw (RuntimeException) e.getTargetException(); - } - throw new SpoonException("Invokation target exception TokenWriter method", e); - } - }); + if (paramTypes.length == 1) { + if (paramTypes[0] == String.class) { + //writeXxx(String) + onTokenWriterWrite(method.getName(), (String) args[0], null, printAction); + } else if (paramTypes[0] == CtComment.class) { + //writeComment(CtComment) + onTokenWriterWrite(method.getName(), null, (CtComment) args[0], printAction); + } + return proxy; + } else if (paramTypes.length == 0) { + //writeSpace() and writeln() + String token = method.getName().equals("writeSpace") ? " " : "\n"; + onTokenWriterWrite(method.getName(), token, null, printAction); return proxy; } } - Object result = method.invoke(tokenWriter, args); - if (method.getReturnType() == TokenWriter.class) { + if (method.getName().endsWith("Tab")) { + //incTab(), decTab() + onTokenWriterWrite(method.getName(), (String) null, null, printAction); return proxy; } - return result; + if (method.getName().equals("getPrinterHelper") || method.getName().equals("reset")) { + try { + return method.invoke(tokenWriter, args); + } catch (IllegalAccessException | IllegalArgumentException e) { + throw new SpoonException("Cannot invoke TokenWriter method", e); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } + throw new SpoonException("Unexpected method TokenWriter#" + method.getName()); } }); } @@ -96,10 +119,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl * Is called for each printed token * @param tokenWriterMethodName the name of {@link TokenWriter} method * @param token the actual token value + * @param comment TODO * @param printAction the executor of the action, we are listening for. * @throws Exception */ - protected void onTokenWriterWrite(String tokenWriterMethodName, String token, Runnable printAction) { + protected void onTokenWriterWrite(String tokenWriterMethodName, String token, CtComment comment, Runnable printAction) { SourceFragmentContext sfc = sourceFragmentContextStack.peek(); if (sfc != null) { sfc.onTokenWriterToken(tokenWriterMethodName, token, printAction); @@ -116,7 +140,7 @@ public ChangesAwareDefaultJavaPrettyPrinter scan(CtElement element) { if (sfc != null) { CtRole role = element.getRoleInParent(); if (role != null) { - sfc.onScanRole(role, () -> scanInternal(element)); + sfc.onScanElementOnRole(element, role, () -> scanInternal(element)); return this; } } diff --git a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java index 895203b70bc..3ee2451244e 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java @@ -110,11 +110,20 @@ public SourcePosition getSourcePosition() { @Override public String toString() { + return getSourceCode(getStart(), getEnd()); + } + + /** + * @param start start offset + * @param end end offset (after last element) + * @return source code of this Fragment between start/end offsets + */ + public String getSourceCode(int start, int end) { CompilationUnit cu = sourcePosition.getCompilationUnit(); if (cu != null) { String src = cu.getOriginalSourceCode(); if (src != null) { - return src.substring(getStart(), getEnd()); + return src.substring(start, end); } } return null; @@ -420,4 +429,28 @@ public SourceFragment getNextFragmentOfSameElement() { } return null; } + /** + * @return source code of this fragment before the first child of this fragment. + * null if there is no child + */ + public String getTextBeforeFirstChild() { + if (firstChild != null) { + return getSourceCode(getStart(), firstChild.getStart()); + } + return null; + } + /** + * @return source code of this fragment after the last child of this fragment + * null if there is no child + */ + public String getTextAfterLastChild() { + SourceFragment lastChild = firstChild; + while (lastChild != null) { + if (lastChild.getNextSibling() == null) { + return getSourceCode(lastChild.getEnd(), getEnd()); + } + lastChild = lastChild.getNextSibling(); + } + return null; + } } diff --git a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContext.java b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContext.java index c9ce60e4147..d2a110e43c0 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContext.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContext.java @@ -1,8 +1,26 @@ package spoon.reflect.visitor.printer.change; +import spoon.reflect.declaration.CtElement; import spoon.reflect.path.CtRole; +import spoon.reflect.visitor.DefaultJavaPrettyPrinter; abstract class SourceFragmentContext { + /** + * Called when TokenWriter token is sent by {@link DefaultJavaPrettyPrinter} + * @param tokenWriterMethodName the name of token method + * @param token the value of token + * @param printAction the {@link Runnable}, which will send the token to the output + */ abstract void onTokenWriterToken(String tokenWriterMethodName, String token, Runnable printAction); - abstract void onScanRole(CtRole role, PrintAction printAction); + /** + * Called when {@link DefaultJavaPrettyPrinter} starts scanning of `element` on the parent`s role `role` + * @param element to be scanned element + * @param role the attribute where the element is in parent + * @param printAction the {@link Runnable}, which will scan that element in {@link DefaultJavaPrettyPrinter} + */ + abstract void onScanElementOnRole(CtElement element, CtRole role, Runnable printAction); + /** + * Called when this is child context and parent context is just going to finish it's printing + */ + abstract void onParentFinished(); } diff --git a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextList.java b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextList.java new file mode 100644 index 00000000000..aae7f1a88f5 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextList.java @@ -0,0 +1,161 @@ +package spoon.reflect.visitor.printer.change; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import spoon.reflect.declaration.CtElement; +import spoon.reflect.meta.impl.RoleHandlerHelper; +import spoon.reflect.path.CtRole; +import spoon.reflect.visitor.printer.change.SourcePositionUtils.FragmentDescriptor; + +/** + * Handles printing of changes of the list of elements + */ +class SourceFragmentContextList extends SourceFragmentContext { + private final ChangesAwareDefaultJavaPrettyPrinter printer; + private final SourceFragment rootFragment; + private final Map listElementToFragmentPair = new IdentityHashMap<>(); + private final List separatorActions = new ArrayList<>(); + //-1 means print start list separator + //0..n print elements of list + private int elementIndex = -1; + + private static class FragmentPair { + /** + * optional spaces before the element (e.g. spaces before the method or field) + */ + String prefixSpaces; + /** + * the fragment of element + */ + SourceFragment elementFragment; + + FragmentPair(String prefixSpaces, SourceFragment elementFragment) { + super(); + this.prefixSpaces = prefixSpaces; + this.elementFragment = elementFragment; + } + } + + SourceFragmentContextList(ChangesAwareDefaultJavaPrettyPrinter changesAwareDefaultJavaPrettyPrinter, CtElement element, SourceFragment rootFragment) { + super(); + printer = changesAwareDefaultJavaPrettyPrinter; + this.rootFragment = rootFragment; + CtRole listRole = rootFragment.fragmentDescriptor.getListRole(); + List listElements = RoleHandlerHelper.getRoleHandler(element.getClass(), listRole).asList(element); + for (CtElement ctElement : listElements) { + SourceFragment elementFragment = SourcePositionUtils.getSourceFragmentOfElement(ctElement); + if (elementFragment != null) { + FragmentPair fragmentPair = createFragmentPair(rootFragment, elementFragment); + if (fragmentPair != null) { + listElementToFragmentPair.put(ctElement, fragmentPair); + } + } + } + } + + private FragmentPair createFragmentPair(SourceFragment rootFragment, SourceFragment elementFragment) { + SourceFragment child = rootFragment.getFirstChild(); + SourceFragment lastChild = null; + while (child != null) { + if (child == elementFragment) { + return new FragmentPair( + lastChild == null ? null : rootFragment.getSourceCode(lastChild.getEnd(), elementFragment.getStart()), + elementFragment); + } + lastChild = child; + child = child.getNextSibling(); + } + return null; + } + + boolean testFagmentDescriptor(SourceFragment sourceFragment, Predicate predicate) { + if (sourceFragment != null) { + if (sourceFragment.fragmentDescriptor != null) { + return predicate.test(sourceFragment.fragmentDescriptor); + } + } + return false; + } + + @Override + void onTokenWriterToken(String tokenWriterMethodName, String token, Runnable printAction) { + if (elementIndex == -1) { + if (printer.mutableTokenWriter.isMuted() == false) { + //print list prefix + String prefix = rootFragment.getTextBeforeFirstChild(); + if (prefix != null) { + //we have origin source code for that + printer.mutableTokenWriter.getPrinterHelper().directPrint(prefix); + //ignore all list prefix tokens + printer.mutableTokenWriter.setMuted(true); + } + } + //print the list prefix actions. It can be more token writer events ... + //so enter it several times. Note: it may be muted, then these tokens are ignored + printAction.run(); + } else { + //print list separator + //collect (postpone) printAction of separators and list ending token, because we print them depending on next element / end of list + separatorActions.add(printAction); + } + } + + @Override + void onScanElementOnRole(CtElement element, CtRole role, Runnable printAction) { + //the printing of child element must not be muted here + //it can be muted later internally, if element is not modified + //but something in list is modified and we do not know what + printer.mutableTokenWriter.setMuted(false); + elementIndex++; + if (elementIndex > 0) { + // print spaces between elements + FragmentPair fragmentPair = listElementToFragmentPair.get(element); + if (fragmentPair != null) { + //there is origin fragment for `element` + if (fragmentPair.prefixSpaces != null) { + //there are origin spaces before this `element` + //use them + printer.mutableTokenWriter.getPrinterHelper().directPrint(fragmentPair.prefixSpaces); + //forget DJPP spaces + separatorActions.clear(); + } + /* + * else there are no origin spaces we have to let it print normally + * - e.g. when new first element is added, then second element has no standard spaces + */ + } + } //else it is the first element of list. Do not print spaces here (we already have spaces after the list prefix) + printStandardSpaces(); + //run the DJPP scanning action, which we are listening for + printAction.run(); + //the child element is printed, now it will print separators or list end + printer.mutableTokenWriter.setMuted(true); + } + + @Override + void onParentFinished() { + //we are the end of the list of elements. Printer just tries to print list suffix and parent fragment detected that + printer.mutableTokenWriter.setMuted(false); + //print list suffix + String suffix = rootFragment.getTextAfterLastChild(); + if (suffix != null) { + //we have origin source code for that list suffix + printer.mutableTokenWriter.getPrinterHelper().directPrint(suffix); + separatorActions.clear(); + } else { + //printer must print the spaces and suffix. Send collected events + printStandardSpaces(); + } + } + + private void printStandardSpaces() { + for (Runnable runnable : separatorActions) { + runnable.run(); + } + separatorActions.clear(); + } +} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextNormal.java b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextNormal.java index b5e38b032ed..84c04f019a9 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextNormal.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragmentContextNormal.java @@ -14,6 +14,10 @@ class SourceFragmentContextNormal extends SourceFragmentContext { private final ChangesAwareDefaultJavaPrettyPrinter printer; private SourceFragment currentFragment; private CtElement element; + /** + * handles printing of lists of elements. E.g. type members of type. + */ + private SourceFragmentContext childContext; SourceFragmentContextNormal(ChangesAwareDefaultJavaPrettyPrinter changesAwareDefaultJavaPrettyPrinter, CtElement element, SourceFragment rootFragment) { super(); @@ -52,15 +56,14 @@ void handlePrinting() { printer.mutableTokenWriter.setMuted(true); } else { //we are printing modified fragment. + printer.mutableTokenWriter.setMuted(false); switch (currentFragment.fragmentDescriptor.kind) { case NORMAL: //Let it print normally - printer.mutableTokenWriter.setMuted(false); break; case LIST: - //we are printing list - //TODO - printer.mutableTokenWriter.setMuted(false); + //we are printing list, create a child context for the list + childContext = new SourceFragmentContextList(printer, element, currentFragment); break; default: throw new SpoonException("Unexpected fragment kind " + currentFragment.fragmentDescriptor.kind); @@ -84,27 +87,34 @@ void onTokenWriterToken(String tokenWriterMethodName, String token, Runnable pri //yes, the next fragment should be activated before printAction nextFragment(); } - //run the print action, which we are listening for - printAction.run(); + if (childContext != null) { + childContext.onTokenWriterToken(tokenWriterMethodName, token, printAction); + } else { + //run the print action, which we are listening for + printAction.run(); + } if (testFagmentDescriptor(currentFragment, fd -> fd.isTriggeredByToken(false, tokenWriterMethodName, token))) { //yes, the next fragment should be activated before printAction + if (childContext != null) { + //notify childContext that it finished + childContext.onParentFinished(); + childContext = null; + } nextFragment(); } } @Override - void onScanRole(CtRole role, PrintAction printAction) { + void onScanElementOnRole(CtElement element, CtRole role, Runnable printAction) { if (testFagmentDescriptor(getNextFragment(), fd -> fd.isStartedByScanRole(role))) { //yes, the next fragment should be activated before printAction nextFragment(); } //run the print action, which we are listening for - try { + if (childContext != null) { + childContext.onScanElementOnRole(element, role, printAction); + } else { printAction.run(); - } catch (SpoonException e) { - throw (SpoonException) e; - } catch (Exception e) { - throw new SpoonException(e); } // { // //check if current fragment has to be finished after this action @@ -113,4 +123,10 @@ void onScanRole(CtRole role, PrintAction printAction) { // } // } } + + @Override + void onParentFinished() { + //we will see if it is true... I (Pavel) am not sure yet + throw new SpoonException("SourceFragmentContextNormal shouldn't be used as child context"); + } } diff --git a/src/main/java/spoon/reflect/visitor/printer/change/SourcePositionUtils.java b/src/main/java/spoon/reflect/visitor/printer/change/SourcePositionUtils.java index cbdab6aad04..641950e412e 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourcePositionUtils.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourcePositionUtils.java @@ -140,11 +140,7 @@ enum FragmentKind { /** * a fragment, which contains a list of elements */ - LIST, - /** - * a fragment, which contains no model element, but contains separators or spaces. - */ - SEPARATOR + LIST } /** @@ -226,6 +222,16 @@ boolean isTriggeredByToken(boolean isStart, String tokenWriterMethodName, String boolean isStartedByScanRole(CtRole role) { return startScanRole.contains(role); } + + /** + * @return {@link CtRole} of the list attribute handled by this fragment + */ + public CtRole getListRole() { + if (kind != FragmentKind.LIST || roles == null || roles.size() != 1) { + throw new SpoonException("This fragment does not have list role"); + } + return roles.iterator().next(); + } } /**