diff --git a/src/main/java/spoon/experimental/modelobs/ChangeCollector.java b/src/main/java/spoon/experimental/modelobs/ChangeCollector.java index ee56f802dc0..b066c0346ec 100644 --- a/src/main/java/spoon/experimental/modelobs/ChangeCollector.java +++ b/src/main/java/spoon/experimental/modelobs/ChangeCollector.java @@ -123,7 +123,12 @@ CtRole getScannedRole() { } } - private void onChange(CtElement currentElement, CtRole role) { + /** + * Called whenever anything changes in the spoon model + * @param currentElement the modified element + * @param role the modified attribute of that element + */ + protected void onChange(CtElement currentElement, CtRole role) { Set roles = elementToChangeRole.get(currentElement); if (roles == null) { roles = new HashSet<>(); diff --git a/src/main/java/spoon/experimental/modelobs/SourceFragmentsTreeCreatingChangeCollector.java b/src/main/java/spoon/experimental/modelobs/SourceFragmentsTreeCreatingChangeCollector.java new file mode 100644 index 00000000000..4af9e4ac387 --- /dev/null +++ b/src/main/java/spoon/experimental/modelobs/SourceFragmentsTreeCreatingChangeCollector.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2006-2017 INRIA and contributors + * Spoon - http://spoon.gforge.inria.fr/ + * + * This software is governed by the CeCILL-C License under French law and + * abiding by the rules of distribution of free software. You can use, modify + * and/or redistribute the software under the terms of the CeCILL-C license as + * circulated by CEA, CNRS and INRIA at http://www.cecill.info. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package spoon.experimental.modelobs; + +import spoon.reflect.cu.CompilationUnit; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.path.CtRole; +import spoon.reflect.visitor.printer.change.SourceFragment; + +/** + * Listens on changes on the spoon model and remembers them + * It builds a tree of {@link SourceFragment}s of {@link CompilationUnit} of the modified element + * lazily before the element is changed + */ +public class SourceFragmentsTreeCreatingChangeCollector extends ChangeCollector { + @Override + protected void onChange(CtElement currentElement, CtRole role) { + CompilationUnit cu = currentElement.getPosition().getCompilationUnit(); + if (cu != null) { + //build a tree of SourceFragments of compilation unit of the modified element before the first change + cu.getRootSourceFragment(); + } + super.onChange(currentElement, role); + } +} diff --git a/src/main/java/spoon/reflect/cu/CompilationUnit.java b/src/main/java/spoon/reflect/cu/CompilationUnit.java index eb130f932f8..afb3918aaf8 100644 --- a/src/main/java/spoon/reflect/cu/CompilationUnit.java +++ b/src/main/java/spoon/reflect/cu/CompilationUnit.java @@ -20,6 +20,8 @@ import spoon.reflect.declaration.CtModule; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; +import spoon.reflect.visitor.printer.change.SourceFragment; +import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtImport; import java.io.File; @@ -55,6 +57,16 @@ enum UNIT_TYPE { */ void setFile(File file); + /** + * @return array of offsets in the origin source file, where occurs line separator + */ + int[] getLineSeparatorPositions(); + + /** + * @param lineSeparatorPositions array of offsets in the origin source file, where occurs line separator + */ + void setLineSeparatorPositions(int[] lineSeparatorPositions); + /** * Gets all binary (.class) files that corresponds to this compilation unit * and have been created by calling @@ -149,4 +161,18 @@ enum UNIT_TYPE { */ void setImports(Collection imports); + /** + * @return root {@link SourceFragment} of the tree of all the source fragments of origin source code + * Note: this method creates tree of {@link SourceFragment}s ... it can be a lot of instances + * If the tree of {@link SourceFragment}s is needed then it MUST be created + * BEFORE the Spoon model of this {@link CompilationUnit} is changed. + * Otherwise there might be missing some {@link SourceFragment}s when {@link CtElement}s are deleted + */ + SourceFragment getRootSourceFragment(); + + /** + * @param element the {@link CtElement} whose origin source code is needed + * @return {@link SourceFragment} which mirrors the origin source code of the `element` or null. + */ + SourceFragment getSourceFragment(CtElement element); } diff --git a/src/main/java/spoon/reflect/cu/SourcePosition.java b/src/main/java/spoon/reflect/cu/SourcePosition.java index a412bcdc3dc..1db3422f0a7 100644 --- a/src/main/java/spoon/reflect/cu/SourcePosition.java +++ b/src/main/java/spoon/reflect/cu/SourcePosition.java @@ -83,16 +83,4 @@ public interface SourcePosition extends Serializable { * Gets the index at which the position starts in the source file. */ int getSourceStart(); - - /** - * @return {@link SourcePosition} of next element in the origin source code. - * If there is no one, then it returns null. It never returns {@link NoSourcePosition} - */ - SourcePosition getNextSibling(); - - /** - * @return {@link SourcePosition} of first child element in the origin source code. - * If there is no one, then it returns null. It never returns {@link NoSourcePosition} - */ - SourcePosition getFirstChild(); } diff --git a/src/main/java/spoon/reflect/cu/position/NoSourcePosition.java b/src/main/java/spoon/reflect/cu/position/NoSourcePosition.java index 66d94059607..8adfa2deaf0 100644 --- a/src/main/java/spoon/reflect/cu/position/NoSourcePosition.java +++ b/src/main/java/spoon/reflect/cu/position/NoSourcePosition.java @@ -73,14 +73,4 @@ public int getSourceStart() { public String toString() { return "(unknown file)"; } - - @Override - public SourcePosition getFirstChild() { - return null; - } - - @Override - public SourcePosition getNextSibling() { - return null; - } } 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 1b8694f8cbb..076562965e0 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/ChangesAwareDefaultJavaPrettyPrinter.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/ChangesAwareDefaultJavaPrettyPrinter.java @@ -20,9 +20,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayDeque; -import java.util.Collections; import java.util.Deque; -import java.util.List; import java.util.function.Predicate; import spoon.SpoonException; @@ -103,30 +101,36 @@ protected void onTokenWriterWrite(String tokenWriterMethodName, String token, Pr } private class SourceFragmentContext { - final List fragments; - int currentFragmentIdx = -1; + private SourceFragment currentFragment; + private CtElement element; - SourceFragmentContext(List fragments) { + SourceFragmentContext(CtElement element, SourceFragment rootFragment) { super(); - this.fragments = fragments; + this.element = element; + this.currentFragment = rootFragment; } SourceFragmentContext() { - this.fragments = Collections.emptyList(); - currentFragmentIdx = 0; + currentFragment = null; + } + + SourceFragment getNextFragment() { + if (currentFragment != null) { + return currentFragment.getNextFragmentOfSameElement(); + } + return null; } /** * Called when next fragment is going to be printed */ void nextFragment() { - currentFragmentIdx++; - if (currentFragmentIdx < fragments.size()) { - SourceFragment fragment = fragments.get(currentFragmentIdx); - if (fragment.isModified() == false) { + currentFragment = getNextFragment(); + if (currentFragment != null) { + if (currentFragment.isModified() == false) { //we are going to print not modified fragment //print origin sources of this fragment directly - mutableTokenWriter.getPrinterHelper().directPrint(fragment.toString()); + mutableTokenWriter.getPrinterHelper().directPrint(currentFragment.toString()); mutableTokenWriter.setMuted(true); } else { //we are printing modified fragment. Let it print normally @@ -135,9 +139,8 @@ void nextFragment() { } } - boolean testFagmentDescriptor(int fragmentIdx, Predicate predicate) { - if (fragmentIdx < fragments.size()) { - SourceFragment sourceFragment = fragments.get(fragmentIdx); + boolean testFagmentDescriptor(SourceFragment sourceFragment, Predicate predicate) { + if (sourceFragment != null) { if (sourceFragment.fragmentDescriptor != null) { return predicate.test(sourceFragment.fragmentDescriptor); } @@ -146,20 +149,20 @@ boolean testFagmentDescriptor(int fragmentIdx, Predicate pre } void onTokenWriterToken(String tokenWriterMethodName, String token, PrintAction printAction) throws Exception { - if (testFagmentDescriptor(currentFragmentIdx + 1, fd -> fd.isTriggeredByToken(true, tokenWriterMethodName, token))) { + if (testFagmentDescriptor(getNextFragment(), fd -> fd.isTriggeredByToken(true, tokenWriterMethodName, token))) { //yes, the next fragment should be activated before printAction nextFragment(); } //run the print action, which we are listening for printAction.run(); - if (testFagmentDescriptor(currentFragmentIdx, fd -> fd.isTriggeredByToken(false, tokenWriterMethodName, token))) { + if (testFagmentDescriptor(currentFragment, fd -> fd.isTriggeredByToken(false, tokenWriterMethodName, token))) { //yes, the next fragment should be activated before printAction nextFragment(); } } void onScanRole(CtRole role, PrintAction printAction) { - if (testFagmentDescriptor(currentFragmentIdx + 1, fd -> fd.isStartedByScanRole(role))) { + if (testFagmentDescriptor(getNextFragment(), fd -> fd.isStartedByScanRole(role))) { //yes, the next fragment should be activated before printAction nextFragment(); } @@ -173,7 +176,6 @@ void onScanRole(CtRole role, PrintAction printAction) { } // { // //check if current fragment has to be finished after this action -// SourceFragment currentFragment = fragments.get(currentFragmentIdx); // if (currentFragment.fragmentDescriptor.isFinishedByScanRole(role)) { // nextFragment(); // } @@ -208,8 +210,8 @@ private void scanInternal(CtElement element) { } //it is not muted yet, so some this element or any sibling was modified //detect SourceFragments of element and whether they are modified or not - List fragments = SourcePositionUtils.getSourceFragmentsOfElement(changeCollector, element); - if (fragments.isEmpty()) { + SourceFragment rootFragmentOfElement = SourcePositionUtils.getSourceFragmentsOfElement(changeCollector, element); + if (rootFragmentOfElement == null) { //we have no origin sources. Use normal printing sourceFragmentContextStack.push(EMPTY_FRAGMENT_CONTEXT); super.scan(element); @@ -217,7 +219,7 @@ private void scanInternal(CtElement element) { return; } try { - SourceFragmentContext sfx = new SourceFragmentContext(fragments); + SourceFragmentContext sfx = new SourceFragmentContext(element, rootFragmentOfElement); sourceFragmentContextStack.push(sfx); //move to the first fragment - and handle printing of modified or not modified content sfx.nextFragment(); 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 e6e838bb62a..895203b70bc 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java @@ -16,41 +16,92 @@ */ package spoon.reflect.visitor.printer.change; +import java.util.ArrayDeque; +import java.util.Deque; + +import spoon.SpoonException; import spoon.reflect.cu.CompilationUnit; import spoon.reflect.cu.SourcePosition; +import spoon.reflect.cu.position.BodyHolderSourcePosition; +import spoon.reflect.cu.position.DeclarationSourcePosition; import spoon.reflect.declaration.CtElement; +import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.printer.change.SourcePositionUtils.FragmentDescriptor; +import spoon.support.reflect.cu.position.SourcePositionImpl; /** * Represents a part of source code of an {@link CtElement} */ -class SourceFragment { +public class SourceFragment { + private final CtElement element; private final SourcePosition sourcePosition; private final FragmentType fragmentType; - private final int start; - private final int end; private boolean modified = false; FragmentDescriptor fragmentDescriptor; + private SourceFragment nextSibling; + private SourceFragment firstChild; - SourceFragment(SourcePosition sourcePosition, FragmentType fragmentType, int start, int end) { + public SourceFragment(CtElement element, SourcePosition sourcePosition) { + this(element, sourcePosition, FragmentType.MAIN_FRAGMENT); + } + public SourceFragment(CtElement element, SourcePosition sourcePosition, FragmentType fragmentType) { super(); + this.element = element; this.sourcePosition = sourcePosition; this.fragmentType = fragmentType; - this.start = start; - this.end = end; + if (fragmentType == FragmentType.MAIN_FRAGMENT) { + createChildFragments(); + } } public FragmentType getFragmentType() { return fragmentType; } + /** + * @return offset of first character which belongs to this fragmen + */ public int getStart() { - return start; + switch (fragmentType) { + case MAIN_FRAGMENT: + return sourcePosition.getSourceStart(); + case MODIFIERS: + return ((DeclarationSourcePosition) sourcePosition).getModifierSourceStart(); + case BEFORE_NAME: + return ((DeclarationSourcePosition) sourcePosition).getModifierSourceEnd() + 1; + case NAME: + return ((DeclarationSourcePosition) sourcePosition).getNameStart(); + case AFTER_NAME: + return ((DeclarationSourcePosition) sourcePosition).getNameEnd() + 1; + case BODY: + return ((BodyHolderSourcePosition) sourcePosition).getBodyStart(); + } + throw new SpoonException("Unsupported fragment type: " + fragmentType); } + /** + * @return offset of first next character after this Fragment + */ public int getEnd() { - return end; + switch (fragmentType) { + case MAIN_FRAGMENT: + return sourcePosition.getSourceEnd() + 1; + case MODIFIERS: + return ((DeclarationSourcePosition) sourcePosition).getModifierSourceEnd() + 1; + case BEFORE_NAME: + return ((DeclarationSourcePosition) sourcePosition).getNameStart(); + case NAME: + return ((DeclarationSourcePosition) sourcePosition).getNameEnd() + 1; + case AFTER_NAME: + if (sourcePosition instanceof BodyHolderSourcePosition) { + return ((BodyHolderSourcePosition) sourcePosition).getBodyStart(); + } + return sourcePosition.getSourceEnd() + 1; + case BODY: + return ((BodyHolderSourcePosition) sourcePosition).getBodyEnd() + 1; + } + throw new SpoonException("Unsupported fragment type: " + fragmentType); } public SourcePosition getSourcePosition() { @@ -63,7 +114,7 @@ public String toString() { if (cu != null) { String src = cu.getOriginalSourceCode(); if (src != null) { - return src.substring(start, end); + return src.substring(getStart(), getEnd()); } } return null; @@ -76,4 +127,297 @@ public boolean isModified() { public void setModified(boolean modified) { this.modified = modified; } + + /** + * @return true if position points to same compilation unit (source file) as this SourceFragment + */ + private boolean isFromSameSource(SourcePosition position) { + return sourcePosition.getCompilationUnit().equals(position.getCompilationUnit()); + } + + /** + * Builds tree of {@link SourcePosition} elements + * @param element the root element of the tree + */ + public void addSourceFragments(CtElement element) { + SourcePosition sp = element.getPosition(); + Deque parents = new ArrayDeque<>(); + parents.push(this); + new CtScanner() { + int noSource = 0; + @Override + protected void enter(CtElement e) { + SourceFragment newFragment = addChild(parents.peek(), e); + if (newFragment != null) { + parents.push(newFragment); + } else { + noSource++; + } + } + @Override + protected void exit(CtElement e) { + if (noSource == 0) { + parents.pop(); + } else { + noSource--; + } + } + }.scan(element); + } + private static SourceFragment addChild(SourceFragment thisFragment, CtElement otherElement) { + SourcePosition otherSourcePosition = otherElement.getPosition(); + if (otherSourcePosition instanceof SourcePositionImpl && otherSourcePosition.getCompilationUnit() != null) { + SourcePositionImpl childSPI = (SourcePositionImpl) otherSourcePosition; + if (thisFragment.sourcePosition != childSPI) { + if (thisFragment.isFromSameSource(otherSourcePosition)) { + SourceFragment otherFragment = new SourceFragment(otherElement, otherSourcePosition, FragmentType.MAIN_FRAGMENT); + //parent and child are from the same file. So we can connect their positions into one tree + CMP cmp = thisFragment.compare(otherFragment); + if (cmp == CMP.OTHER_IS_CHILD) { + //child belongs under parent - OK + thisFragment.addChild(otherFragment); + } else { + //the source position of child element is not included in source position of parent element +// if (otherElement instanceof CtAnnotation) { +// /* +// * it can happen for annotations of type TYPE_USE and FIELD +// * In such case the annotation belongs to 2 elements +// * And one of them cannot have matching source position - OK +// */ +// return null; +// } + //something is wrong ... + SourcePosition.class.getClass(); + throw new SpoonException("TODO"); + } + } else { + throw new SpoonException("SourcePosition from unexpected compilation unit: " + otherSourcePosition + " expected is: " + thisFragment.sourcePosition); + } + } + //else these two elements has same instance of SourcePosition. + //It is probably OK. Do not created new SourceFragment for that + return null; + } + //do not connect that undefined source position + return null; + } + + /** + * adds `other` {@link SourceFragment} into tree of {@link SourceFragment}s represented by this root element + * @param other to be added {@link SourceFragment} + * @return new root of the tree of the {@link SourceFragment}s. It can be be this or `other` + */ + public SourceFragment add(SourceFragment other) { + if (this == other) { + throw new SpoonException("SourceFragment#add must not be called twice for the same SourceFragment"); + //optionally we might accept that and simply return this + } + CMP cmp = this.compare(other); + switch (cmp) { + case OTHER_IS_AFTER: + //other is after this + addNextSibling(other); + return this; + case OTHER_IS_BEFORE: + //other is before this + other.addNextSibling(this); + return other; + case OTHER_IS_CHILD: + //other is child of this + addChild(other); + return this; + case OTHER_IS_PARENT: + //other is parent of this + other.addChild(this); + //merge siblings of `this` as children and siblings of `other` + other.mergeSiblingsOfChild(this); + return other; + } + throw new SpoonException("Unexpected compare result: " + cmp); + } + + private void mergeSiblingsOfChild(SourceFragment other) { + SourceFragment prevOther = other; + other = prevOther.getNextSibling(); + while (other != null) { + CMP cmp = compare(other); + if (cmp == CMP.OTHER_IS_CHILD) { + //ok, it is child too. Keep it as sibling of children + prevOther = other; + other = other.getNextSibling(); + continue; + } else if (cmp == CMP.OTHER_IS_AFTER) { + //the next sibling of child is after `this` + //disconnect it from prevOther and connect it as sibling of this + prevOther.nextSibling = null; + addNextSibling(other); + //and we are done, because other.nextSibling is already OK + } + throw new SpoonException("Unexpected child SourceFragment"); + } + } + + private void addChild(SourceFragment child) { + if (firstChild == null) { + firstChild = child; + } else { + firstChild = firstChild.add(child); + } + } + + private void addNextSibling(SourceFragment sibling) { + if (nextSibling == null) { + nextSibling = sibling; + } else { + nextSibling = nextSibling.add(sibling); + } + } + + private enum CMP { + OTHER_IS_BEFORE, + OTHER_IS_AFTER, + OTHER_IS_CHILD, + OTHER_IS_PARENT + } + + /** + * compares this and other + * @param other other {@link SourcePosition} + * @return CMP + * throws {@link SpoonException} if intervals overlap or start/end is negative + */ + private CMP compare(SourceFragment other) { + if (other == this) { + throw new SpoonException("SourcePositionImpl#addNextSibling must not be called twice for the same SourcePosition"); + } + if (getEnd() <= other.getStart()) { + //other is after this + return CMP.OTHER_IS_AFTER; + } + if (other.getEnd() <= getStart()) { + //other is before this + return CMP.OTHER_IS_BEFORE; + } + if (getStart() <= other.getStart() && getEnd() >= other.getEnd()) { + //other is child of this + return CMP.OTHER_IS_CHILD; + } + if (getStart() >= other.getStart() && getEnd() <= other.getEnd()) { + //other is parent of this + return CMP.OTHER_IS_PARENT; + } + //the fragments overlap - it is not allowed + throw new SpoonException("Cannot compare this: [" + getStart() + ", " + getEnd() + "] with other: [\"" + other.getStart() + "\", \"" + other.getEnd() + "\"]"); + } + + private void createChildFragments() { + if (sourcePosition instanceof DeclarationSourcePosition) { + DeclarationSourcePosition dsp = (DeclarationSourcePosition) sourcePosition; + int endOfLastFragment = dsp.getSourceStart(); + if (endOfLastFragment < dsp.getModifierSourceStart()) { + throw new SpoonException("DeclarationSourcePosition#sourceStart < modifierSourceStart for: " + sourcePosition); + } + addChild(new SourceFragment(element, sourcePosition, FragmentType.MODIFIERS)); + if (endOfLastFragment < dsp.getNameStart()) { + addChild(new SourceFragment(element, sourcePosition, FragmentType.BEFORE_NAME)); + } + addChild(new SourceFragment(element, sourcePosition, FragmentType.NAME)); + if (dsp instanceof BodyHolderSourcePosition) { + BodyHolderSourcePosition bhsp = (BodyHolderSourcePosition) dsp; + if (endOfLastFragment < bhsp.getBodyStart()) { + addChild(new SourceFragment(element, sourcePosition, FragmentType.AFTER_NAME)); + } + SourceFragment bodyFragment = new SourceFragment(element, sourcePosition, FragmentType.BODY); + addChild(bodyFragment); + if (bodyFragment.getEnd() != bhsp.getBodyEnd() + 1) { + throw new SpoonException("Last bodyEnd is not equal to SourcePosition#sourceEnd: " + sourcePosition); + } + } else { + if (endOfLastFragment < dsp.getSourceEnd() + 1) { + addChild(new SourceFragment(element, sourcePosition, FragmentType.AFTER_NAME)); + } + } + } + } + public SourceFragment getNextSibling() { + return nextSibling; + } + public SourceFragment getFirstChild() { + return firstChild; + } + + /** + * Searches the tree of {@link SourceFragment}s + * It searches in siblings and children of this {@link SourceFragment} recursively. + * + * @param start the start offset of this fragment + * @param end the offset of next character after the end of this fragment + * @return SourceFragment which represents the root of the CtElement whose sources are in interval [start, end] + */ + public SourceFragment getSourceFragmentOf(int start, int end) { + int myEnd = getEnd(); + if (myEnd <= start) { + //search in next sibling + if (nextSibling == null) { + return null; + } + return getRootFragmentOfElement(nextSibling.getSourceFragmentOf(start, end)); + } + int myStart = getStart(); + if (myStart <= start) { + if (myEnd >= end) { + if (myStart == start && myEnd == end) { + //we have found exact match + return this; + } + //it is the child + if (firstChild == null) { + return null; + } + return getRootFragmentOfElement(firstChild.getSourceFragmentOf(start, end)); + } + //start - end overlaps over multiple fragments + throw new SpoonException("Invalid start/end interval. It overlaps multiple fragments."); + } + throw new SpoonException("Invalid start/end interval. It is before this fragment"); + } + + private SourceFragment getRootFragmentOfElement(SourceFragment childFragment) { + if (childFragment != null && getElement() != null && childFragment.getElement() == getElement()) { + //child fragment and this fragment have same element. Return this fragment, + //because we have to return root fragment of CtElement + return this; + } + return childFragment; + } + /** + * @return {@link CtElement} whose source code is contained in this fragment + */ + public CtElement getElement() { + return element; + } + + /** + * @return direct child {@link SourceFragment} if it has same element like this. + * It means that this {@link SourceFragment} knows the source parts of this element + * Else it returns null. + */ + public SourceFragment getChildFragmentOfSameElement() { + if (getFirstChild() != null && getFirstChild().getElement() == element) { + return getFirstChild(); + } + return null; + } + + /** + * @return direct next sibling {@link SourceFragment} if it has same element like this. + * It means that this {@link SourceFragment} knows the next source part of this element + * Else it returns null. + */ + public SourceFragment getNextFragmentOfSameElement() { + if (getNextSibling() != null && getNextSibling().getElement() == element) { + return getNextSibling(); + } + return null; + } } 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 918c07b4129..2589c6d3774 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourcePositionUtils.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourcePositionUtils.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -27,11 +26,9 @@ import java.util.function.BiPredicate; import java.util.function.Consumer; -import spoon.SpoonException; import spoon.experimental.modelobs.ChangeCollector; +import spoon.reflect.cu.CompilationUnit; import spoon.reflect.cu.SourcePosition; -import spoon.reflect.cu.position.BodyHolderSourcePosition; -import spoon.reflect.cu.position.DeclarationSourcePosition; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtExecutable; import spoon.reflect.declaration.CtType; @@ -44,13 +41,26 @@ */ public abstract class SourcePositionUtils { - public static List getSourceFragmentsOfElement(ChangeCollector changeCollector, CtElement element) { - //it is not muted yet, so some child is modified + public static SourceFragment getSourceFragmentOfElement(CtElement element) { + SourcePosition sp = element.getPosition(); + if (sp.getCompilationUnit() != null) { + CompilationUnit cu = sp.getCompilationUnit(); + return cu.getSourceFragment(element); + } + return null; + } + + /** + * @param changeCollector + * @param element + * @return SourceFragment which wraps all the source parts of the `element` + */ + public static SourceFragment getSourceFragmentsOfElement(ChangeCollector changeCollector, CtElement element) { //detect source code fragments of this element - SourcePosition sourcePosition = getNonEmptySourcePosition(element); - if (sourcePosition == null) { - //we have no origin sources. - return Collections.emptyList(); + SourceFragment rootFragmentOfElement = getSourceFragmentOfElement(element); + if (rootFragmentOfElement == null) { + //we have no origin sources for this element + return null; } //The origin sources of this element are available //check if this element was changed @@ -58,25 +68,26 @@ public static List getSourceFragmentsOfElement(ChangeCollector c if (changedRoles.isEmpty()) { //element was not changed and we know origin sources //use origin source instead of printed code - return Collections.singletonList(getMainSourcePositionFragment(element)); + return rootFragmentOfElement; } //element is changed. Detect source fragments of this element - List fragments = SourcePositionUtils.getSourcePositionFragments(element); - if (fragments.size() <= 1) { + SourceFragment childSourceFragmentsOfSameElement = rootFragmentOfElement.getChildFragmentOfSameElement(); + if (childSourceFragmentsOfSameElement == null || childSourceFragmentsOfSameElement.getNextFragmentOfSameElement() == null) { //there is only one source fragment and it is modified. //So we cannot use origin sources - return Collections.emptyList(); + return null; } /* - * there are more fragments. + * there are more fragments. So may be some of them are not modified and we can use origin source to print them + * e.g. when only type members of class are modified, we can still print the class header from the origin sources * Mark which fragments contains source code of data from modified roles */ //detect which roles of this element contains a change - if (markChangedFragments(element, fragments, changedRoles)) { - return fragments; + if (markChangedFragments(element, childSourceFragmentsOfSameElement, changedRoles)) { + return childSourceFragmentsOfSameElement; } //this kind of changes is not supported for this element yet. We cannot use origin sources :-( - return Collections.emptyList(); + return null; } /** @@ -93,58 +104,6 @@ private static SourcePosition getNonEmptySourcePosition(CtElement element) { return null; } - /** - * @param element an {@link CtElement} - * @return {@link SourceFragment} of source code of whole `element` or null if origin sources are not available - */ - private static SourceFragment getMainSourcePositionFragment(CtElement element) { - SourcePosition sp = getNonEmptySourcePosition(element); - if (sp != null) { - return new SourceFragment(sp, FragmentType.MAIN_FRAGMENT, sp.getSourceStart(), sp.getSourceEnd() + 1); - } - return null; - } - /** - * @param element the {@link CtElement} whose fragments has to be analyzed - * @return {@link List} of {@link SourceFragment}s of the `element`. - * If the origin source of element is not available then returns empty list. - */ - private static List getSourcePositionFragments(CtElement element) { - SourcePosition sp = getNonEmptySourcePosition(element); - if (sp != null) { - return getSourcePositionFragments(element, sp); - } - return Collections.emptyList(); - } - - private static List getSourcePositionFragments(CtElement element, SourcePosition sourcePosition) { - if (sourcePosition instanceof DeclarationSourcePosition) { - List fragments = new ArrayList<>(4); - DeclarationSourcePosition dsp = (DeclarationSourcePosition) sourcePosition; - int endOfLastFragment = dsp.getSourceStart(); - if (endOfLastFragment < dsp.getModifierSourceStart()) { - throw new SpoonException("DeclarationSourcePosition#sourceStart < modifierSourceStart for class: " + element.getClass()); - } - fragments.add(new SourceFragment(sourcePosition, FragmentType.MODIFIERS, dsp.getModifierSourceStart(), endOfLastFragment = dsp.getModifierSourceEnd() + 1)); - if (endOfLastFragment < dsp.getNameStart()) { - fragments.add(new SourceFragment(sourcePosition, FragmentType.BEFORE_NAME, endOfLastFragment, dsp.getNameStart())); - } - fragments.add(new SourceFragment(sourcePosition, FragmentType.NAME, dsp.getNameStart(), endOfLastFragment = dsp.getNameEnd() + 1)); - if (dsp instanceof BodyHolderSourcePosition) { - BodyHolderSourcePosition bhsp = (BodyHolderSourcePosition) dsp; - if (endOfLastFragment < bhsp.getBodyStart()) { - fragments.add(new SourceFragment(sourcePosition, FragmentType.AFTER_NAME, endOfLastFragment, bhsp.getBodyStart())); - } - fragments.add(new SourceFragment(sourcePosition, FragmentType.BODY, bhsp.getBodyStart(), endOfLastFragment = bhsp.getBodyEnd() + 1)); - if (endOfLastFragment != bhsp.getBodyEnd() + 1) { - throw new SpoonException("Last bodyEnd is not equal to SourcePosition#sourceEnd: " + element.getClass()); - } - } - return fragments; - } - return Collections.singletonList(new SourceFragment(sourcePosition, FragmentType.MAIN_FRAGMENT, sourcePosition.getSourceStart(), sourcePosition.getSourceEnd() + 1)); - } - private static class TypeToFragmentDescriptor { Class type; Map fragmentToRoles = new HashMap<>(); @@ -297,17 +256,18 @@ private static TypeToFragmentDescriptor type(Class type) { * @return true if {@link SourceFragment}s matches all changed roles, so we can use them * false if current `descriptors` is insufficient and we cannot use origin source code of any fragment */ - private static boolean markChangedFragments(CtElement element, List fragments, Set changedRoles) { + private static boolean markChangedFragments(CtElement element, SourceFragment fragment, Set changedRoles) { for (TypeToFragmentDescriptor descriptor : descriptors) { if (descriptor.matchesElement(element)) { Set toBeAssignedRoles = new HashSet<>(changedRoles); - for (SourceFragment fragment : fragments) { + while (fragment != null) { //check if this fragment is modified FragmentDescriptor fd = descriptor.fragmentToRoles.get(fragment.getFragmentType()); if (fd != null) { //detect if `fragment` is modified and setup fragment start/end detectors fd.applyTo(fragment, toBeAssignedRoles); } + fragment = fragment.getNextFragmentOfSameElement(); } //we can use it if all changed roles are matching to some fragment if (toBeAssignedRoles.isEmpty()) { diff --git a/src/main/java/spoon/support/compiler/jdt/JDTBasedSpoonCompiler.java b/src/main/java/spoon/support/compiler/jdt/JDTBasedSpoonCompiler.java index cf3a20cc2d6..581febabbce 100644 --- a/src/main/java/spoon/support/compiler/jdt/JDTBasedSpoonCompiler.java +++ b/src/main/java/spoon/support/compiler/jdt/JDTBasedSpoonCompiler.java @@ -51,7 +51,6 @@ import spoon.reflect.visitor.Query; import spoon.support.QueueProcessingManager; import spoon.support.compiler.VirtualFolder; -import spoon.support.reflect.cu.position.SourcePositionImpl; import java.io.ByteArrayInputStream; import java.io.File; @@ -121,8 +120,6 @@ public boolean build(JDTBuilder builder) { templateSuccess = buildTemplates(builder); factory.getEnvironment().debugMessage("built in " + (System.currentTimeMillis() - t) + " ms"); checkModel(); - //connect the SourcePosition elements into a tree - SourcePositionImpl.buildTreeOfSourcePositions(factory.getModel().getUnnamedModule()); return srcSuccess && templateSuccess; } diff --git a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java index 25caeb04605..84a61c2705b 100644 --- a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java @@ -61,6 +61,7 @@ SourcePosition buildPositionCtElement(CtElement e, ASTNode node) { CompilationUnit cu = this.jdtTreeBuilder.getFactory().CompilationUnit().getOrCreate(new String(this.jdtTreeBuilder.getContextBuilder().compilationunitdeclaration.getFileName())); CompilationResult cr = this.jdtTreeBuilder.getContextBuilder().compilationunitdeclaration.compilationResult; int[] lineSeparatorPositions = cr.lineSeparatorPositions; + cu.setLineSeparatorPositions(lineSeparatorPositions); char[] contents = cr.compilationUnit.getContents(); diff --git a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java index 3d1401aa2b5..e0b44d627f8 100644 --- a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java +++ b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java @@ -16,15 +16,20 @@ */ package spoon.support.reflect.cu; +import spoon.SpoonException; import spoon.processing.FactoryAccessor; import spoon.reflect.cu.CompilationUnit; +import spoon.reflect.cu.SourcePosition; import spoon.reflect.declaration.CtModule; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; import spoon.reflect.factory.Factory; +import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtImport; import spoon.reflect.visitor.DefaultJavaPrettyPrinter; import spoon.reflect.visitor.filter.TypeFilter; +import spoon.reflect.visitor.printer.change.SourceFragment; +import spoon.support.reflect.cu.position.SourcePositionImpl; import java.io.File; import java.io.FileInputStream; @@ -38,17 +43,21 @@ import static spoon.reflect.ModelElementContainerDefaultCapacities.COMPILATION_UNIT_DECLARED_TYPES_CONTAINER_DEFAULT_CAPACITY; public class CompilationUnitImpl implements CompilationUnit, FactoryAccessor { - Factory factory; + private Factory factory; - List> declaredTypes = new ArrayList<>(COMPILATION_UNIT_DECLARED_TYPES_CONTAINER_DEFAULT_CAPACITY); + private final List> declaredTypes = new ArrayList<>(COMPILATION_UNIT_DECLARED_TYPES_CONTAINER_DEFAULT_CAPACITY); - CtPackage ctPackage; + private CtPackage ctPackage; - Collection imports = new HashSet<>(); + private Collection imports = new HashSet<>(); - CtModule ctModule; + private CtModule ctModule; - File file; + private File file; + + private int[] lineSeparatorPositions; + + private SourceFragment rootFragment; @Override public UNIT_TYPE getUnitType() { @@ -269,6 +278,32 @@ public void setAutoImport(boolean autoImport) { this.autoImport = autoImport; } + public SourceFragment getRootSourceFragment() { + if (rootFragment == null) { + String originSourceCode = getOriginalSourceCode(); + rootFragment = new SourceFragment(null, new SourcePositionImpl(this, 0, originSourceCode.length() - 1, getLineSeparatorPositions())); + for (CtType ctType : declaredTypes) { + rootFragment.addSourceFragments(ctType); + } + } + return rootFragment; + } + @Override + public SourceFragment getSourceFragment(CtElement element) { + SourceFragment rootFragment = getRootSourceFragment(); + SourcePosition sp = element.getPosition(); + if (sp.getCompilationUnit() != this) { + throw new SpoonException("Cannot return SourceFragment of element for different CompilationUnit"); + } + return rootFragment.getSourceFragmentOf(sp.getSourceStart(), sp.getSourceEnd() + 1); + } + public int[] getLineSeparatorPositions() { + return lineSeparatorPositions; + } + + public void setLineSeparatorPositions(int[] lineSeparatorPositions) { + this.lineSeparatorPositions = lineSeparatorPositions; + } } diff --git a/src/main/java/spoon/support/reflect/cu/position/BodyHolderSourcePositionImpl.java b/src/main/java/spoon/support/reflect/cu/position/BodyHolderSourcePositionImpl.java index d3f97ddd8ad..423ddbeb6d8 100644 --- a/src/main/java/spoon/support/reflect/cu/position/BodyHolderSourcePositionImpl.java +++ b/src/main/java/spoon/support/reflect/cu/position/BodyHolderSourcePositionImpl.java @@ -45,6 +45,7 @@ public BodyHolderSourcePositionImpl( modifierSourceStart, modifierSourceEnd, declarationSourceStart, declarationSourceEnd, lineSeparatorPositions); + checkArgsAreAscending(declarationSourceStart, modifierSourceStart, modifierSourceEnd, sourceStart, sourceEnd, bodyStart, bodyEnd, declarationSourceEnd); this.bodyStart = bodyStart; this.bodyEnd = bodyEnd; } diff --git a/src/main/java/spoon/support/reflect/cu/position/DeclarationSourcePositionImpl.java b/src/main/java/spoon/support/reflect/cu/position/DeclarationSourcePositionImpl.java index 6d56ffc3b99..8aa80c0db67 100644 --- a/src/main/java/spoon/support/reflect/cu/position/DeclarationSourcePositionImpl.java +++ b/src/main/java/spoon/support/reflect/cu/position/DeclarationSourcePositionImpl.java @@ -40,6 +40,7 @@ public DeclarationSourcePositionImpl(CompilationUnit compilationUnit, int source super(compilationUnit, sourceStart, sourceEnd, lineSeparatorPositions); + checkArgsAreAscending(declarationSourceStart, modifierSourceStart, modifierSourceEnd, sourceStart, sourceEnd, declarationSourceEnd); this.modifierSourceStart = modifierSourceStart; this.declarationSourceStart = declarationSourceStart; this.declarationSourceEnd = declarationSourceEnd; diff --git a/src/main/java/spoon/support/reflect/cu/position/SourcePositionImpl.java b/src/main/java/spoon/support/reflect/cu/position/SourcePositionImpl.java index eb166235983..6361e0d3155 100644 --- a/src/main/java/spoon/support/reflect/cu/position/SourcePositionImpl.java +++ b/src/main/java/spoon/support/reflect/cu/position/SourcePositionImpl.java @@ -19,17 +19,9 @@ import spoon.SpoonException; import spoon.reflect.cu.CompilationUnit; import spoon.reflect.cu.SourcePosition; -import spoon.reflect.declaration.CtElement; -import spoon.reflect.visitor.CtScanner; -import spoon.reflect.visitor.printer.change.SourcePositionUtils; import java.io.File; import java.io.Serializable; -import java.util.ArrayDeque; -import java.util.Collections; -import java.util.Deque; -import java.util.IdentityHashMap; -import java.util.Set; /** * This immutable class represents the position of a Java program element in a source @@ -119,14 +111,9 @@ private int searchColumnNumber(int position) { /** The file for this position, same pointer as in the compilation unit, but required for serialization. */ private File file; - private SourcePositionImpl nextSibling; - private SourcePositionImpl firstChild; - public SourcePositionImpl(CompilationUnit compilationUnit, int sourceStart, int sourceEnd, int[] lineSeparatorPositions) { super(); - if (sourceStart < 0 || sourceEnd < 0) { - throw new SpoonException("Invalid source position"); - } + checkArgsAreAscending(sourceStart, sourceEnd); this.compilationUnit = compilationUnit; if (compilationUnit != null) { this.file = compilationUnit.getFile(); @@ -221,135 +208,16 @@ public String getSourceInfo() { return getSourceFragment(); } - @Override - public SourcePositionImpl getNextSibling() { - return nextSibling; - } - - @Override - public SourcePositionImpl getFirstChild() { - return firstChild; - } - - /** - * Builds tree of {@link SourcePosition} elements - * @param element the root element of the tree - */ - public static void buildTreeOfSourcePositions(CtElement element) { - SourcePosition sp = element.getPosition(); - Set allElements = Collections.newSetFromMap(new IdentityHashMap<>()); - //TODO check that this method is called only once by the client - new CtScanner() { - Deque parents = new ArrayDeque<>(); - @Override - protected void enter(CtElement e) { - if (allElements.add(e) == false) { - throw new SpoonException("The element is referenced twice"); - } - addChild(parents.peek(), e); - parents.push(e); + protected static void checkArgsAreAscending(int...values) { + int last = -1; + for (int value : values) { + if (value < 0) { + throw new SpoonException("SourcePosition value must not be negative"); } - @Override - protected void exit(CtElement e) { - parents.pop(); + if (last > value) { + throw new SpoonException("SourcePosition values must be ascending or equal"); } - }.scan(element); - } - private static void addChild(CtElement parentElement, CtElement childElement) { - SourcePosition childSP = childElement.getPosition(); - if (childSP instanceof SourcePositionImpl && childSP.getCompilationUnit() != null) { - SourcePositionImpl childSPI = (SourcePositionImpl) childSP; - childSPI.checkValid(); - SourcePositionImpl parentSP = SourcePositionUtils.getMyOrParentsSourcePosition(parentElement); - if (parentSP != null && parentSP.getCompilationUnit() != null) { - if (parentSP != childSPI) { - if (parentSP.getCompilationUnit().equals(childSP.getCompilationUnit())) { - //parent and child are from the same file. So we can connect their positions into one tree - int cmp = parentSP.compare(childSPI); - if (cmp == 0) { - parentSP.addChild(childSPI); - } else { - SourcePosition.class.getClass(); - } - } - } - //else these two elements has same instance of SourcePosition. - //It is probably OK - } - return; - } - //do not connect that undefined source position - return; - } - - private void addChild(SourcePositionImpl child) { - if (firstChild == null) { - firstChild = child; - } else { - firstChild.addNextSibling(child); + last = value; } } - - private void addNextSibling(SourcePositionImpl sibling) { - if (nextSibling == sibling || sibling == this) { - throw new SpoonException("SourcePositionImpl#addNextSibling must not be called twice for the same SourcePosition"); - } - if (sibling.toString().indexOf("|1048;1065|processors == null|") >= 0) { - this.getClass(); - } - if (nextSibling == null) { - nextSibling = sibling; - } else { - int cmp = nextSibling.compare(sibling); - if (cmp == 1) { - //sibling is after nextSibling - nextSibling.addNextSibling(sibling); - } else if (cmp == -1) { - //sibling is before nextSibling - //append sibling before nextSibling - sibling.nextSibling = nextSibling; - nextSibling = sibling; - } else { - //sibling is child of nextSibling - nextSibling.addChild(sibling); - } - } - } - - /** - * compares this and other - * @param other other {@link SourcePosition} - * @return - * -1 - if the other is before this - * 0 - if the other is child of this or equal to this (which means it is child too) - * 1 - if other is after this - * throws {@link SpoonException} if intervals overlap or start/end is negative - */ - private int compare(SourcePosition other) { - if (other == this) { - throw new SpoonException("SourcePositionImpl#addNextSibling must not be called twice for the same SourcePosition"); - } - if (getSourceEnd() < other.getSourceStart()) { - //other is after this - return 1; - } - if (other.getSourceEnd() < getSourceStart()) { - //other is before this - return -1; - } - if (getSourceStart() <= other.getSourceStart() && getSourceEnd() >= other.getSourceEnd()) { - return 0; - } - throw new SpoonException("Cannot compare this: [" + getSourceStart() + ", " + getSourceEnd() + "] with other: [\"" + other.getSourceStart() + "\", \"" + other.getSourceEnd() + "\"]"); - } - - private void checkValid() { - if (getSourceEnd() < 0 || getSourceEnd() < 0) { - throw new SpoonException("Unexpected negative source position"); - } - if (getSourceStart() > getSourceEnd()) { - throw new SpoonException("Invalid start/end. Start is after end."); - } - } - } diff --git a/src/test/java/spoon/test/main/MainTest.java b/src/test/java/spoon/test/main/MainTest.java index 66ddb1b7298..356dbec6740 100644 --- a/src/test/java/spoon/test/main/MainTest.java +++ b/src/test/java/spoon/test/main/MainTest.java @@ -37,6 +37,7 @@ import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.PrinterHelper; import spoon.reflect.visitor.filter.TypeFilter; +import spoon.reflect.visitor.printer.change.SourceFragment; import spoon.test.parent.ParentTest; import java.io.ByteArrayOutputStream; @@ -455,7 +456,7 @@ public void testSourcePositionTreeIsCorrectlyOrdered() { boolean hasComment = false; for (CtType type : types) { SourcePosition sp = type.getPosition(); - totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp); + totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getRootSourceFragment()); hasComment = hasComment || type.getComments().size() > 0; }; assertTrue(totalCount > 1000); @@ -464,19 +465,19 @@ public void testSourcePositionTreeIsCorrectlyOrdered() { /** * Asserts that all siblings and children of sp are well ordered - * @param sp + * @param sourceFragment * @return number of checked {@link SourcePosition} nodes */ - private int assertSourcePositionTreeIsCorrectlyOrder(SourcePosition sp) { + private int assertSourcePositionTreeIsCorrectlyOrder(SourceFragment sourceFragment) { int nr = 0; int pos = 0; - while (sp != null) { + while (sourceFragment != null) { nr++; - assertTrue(pos <= sp.getSourceStart()); - assertTrue(sp.getSourceStart() <= sp.getSourceEnd()); - pos = sp.getSourceEnd(); - nr += assertSourcePositionTreeIsCorrectlyOrder(sp.getFirstChild()); - sp = sp.getNextSibling(); + assertTrue(pos <= sourceFragment.getStart()); + assertTrue(sourceFragment.getStart() <= sourceFragment.getEnd()); + pos = sourceFragment.getEnd(); + nr += assertSourcePositionTreeIsCorrectlyOrder(sourceFragment.getFirstChild()); + sourceFragment = sourceFragment.getNextSibling(); } return nr; } diff --git a/src/test/java/spoon/test/position/PositionTest.java b/src/test/java/spoon/test/position/PositionTest.java index 902eab11fc6..7bc5dd4cdc2 100644 --- a/src/test/java/spoon/test/position/PositionTest.java +++ b/src/test/java/spoon/test/position/PositionTest.java @@ -7,17 +7,21 @@ import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtFieldAccess; import spoon.reflect.code.CtIf; +import spoon.reflect.code.CtLocalVariable; import spoon.reflect.code.CtStatement; +import spoon.reflect.code.CtVariableRead; import spoon.reflect.cu.SourcePosition; import spoon.reflect.cu.position.BodyHolderSourcePosition; import spoon.reflect.cu.position.DeclarationSourcePosition; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtConstructor; +import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtType; import spoon.reflect.factory.Factory; import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.filter.TypeFilter; +import spoon.test.position.testclasses.CastVariableRead; import spoon.test.position.testclasses.Foo; import spoon.test.position.testclasses.FooAbstractMethod; import spoon.test.position.testclasses.FooAnnotation; @@ -30,6 +34,7 @@ import spoon.test.position.testclasses.FooMethod; import spoon.test.position.testclasses.FooStatement; import spoon.test.position.testclasses.PositionParameterTypeWithReference; +import spoon.testing.utils.ModelUtils; import java.io.BufferedReader; import java.io.File; @@ -41,6 +46,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static spoon.testing.utils.ModelUtils.build; import static spoon.testing.utils.ModelUtils.buildClass; @@ -593,4 +599,15 @@ public void getPositionOfImplicitBlock() { assertNotEquals(returnStatement, otherReturnStatement); } + @Test + public void testPositionCastExpressionOfVariableRead() throws Exception { + //contract: the variable read with cast expression has wrong SourcePosition + final CtType foo = ModelUtils.buildClass(CastVariableRead.class); + String classContent = getClassContent(foo); + + CtLocalVariable localVar = (CtLocalVariable) foo.getMethodsByName("m").get(0).getBody().getStatement(1); + CtVariableRead varRead = (CtVariableRead) localVar.getDefaultExpression(); + assertEquals("", contentAtPosition(classContent, varRead.getPosition())); + } + } diff --git a/src/test/java/spoon/test/position/SourceFragmentTest.java b/src/test/java/spoon/test/position/SourceFragmentTest.java new file mode 100644 index 00000000000..057c30e477a --- /dev/null +++ b/src/test/java/spoon/test/position/SourceFragmentTest.java @@ -0,0 +1,163 @@ +package spoon.test.position; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.visitor.printer.change.FragmentType; +import spoon.reflect.visitor.printer.change.SourceFragment; +import spoon.support.reflect.cu.position.BodyHolderSourcePositionImpl; +import spoon.support.reflect.cu.position.DeclarationSourcePositionImpl; +import spoon.support.reflect.cu.position.SourcePositionImpl; + +public class SourceFragmentTest { + + @Test + public void testSourcePositionFragment() throws Exception { + SourcePosition sp = new SourcePositionImpl(null, 10, 20, null); + SourceFragment sf = new SourceFragment(null, sp); + assertEquals(10, sf.getStart()); + assertEquals(21, sf.getEnd()); + assertSame(sp, sf.getSourcePosition()); + assertNull(sf.getFirstChild()); + assertNull(sf.getNextSibling()); + + } + @Test + public void testDeclarationSourcePositionFragment() throws Exception { + SourcePosition sp = new DeclarationSourcePositionImpl(null, 100, 110, 90, 95, 90, 130, null); + SourceFragment sf = new SourceFragment(null, sp); + assertEquals(90, sf.getStart()); + assertEquals(131, sf.getEnd()); + assertSame(sp, sf.getSourcePosition()); + + assertNotNull(sf.getFirstChild()); + assertNull(sf.getNextSibling()); + + SourceFragment sibling; + sibling = sf.getFirstChild(); + assertSame(FragmentType.MODIFIERS, sibling.getFragmentType()); + assertEquals(90, sibling.getStart()); + assertEquals(96, sibling.getEnd()); + + sibling = sibling.getNextSibling(); + assertSame(FragmentType.BEFORE_NAME, sibling.getFragmentType()); + assertEquals(96, sibling.getStart()); + assertEquals(100, sibling.getEnd()); + + sibling = sibling.getNextSibling(); + assertSame(FragmentType.NAME, sibling.getFragmentType()); + assertEquals(100, sibling.getStart()); + assertEquals(111, sibling.getEnd()); + + sibling = sibling.getNextSibling(); + assertSame(FragmentType.AFTER_NAME, sibling.getFragmentType()); + assertEquals(111, sibling.getStart()); + assertEquals(131, sibling.getEnd()); + } + + @Test + public void testBodyHolderSourcePositionFragment() throws Exception { + SourcePosition sp = new BodyHolderSourcePositionImpl(null, 100, 110, 90, 95, 90, 130, 120, 130, null); + SourceFragment sf = new SourceFragment(null, sp); + assertEquals(90, sf.getStart()); + assertEquals(131, sf.getEnd()); + assertSame(sp, sf.getSourcePosition()); + + assertNotNull(sf.getFirstChild()); + assertNull(sf.getNextSibling()); + + SourceFragment sibling; + sibling = sf.getFirstChild(); + assertSame(FragmentType.MODIFIERS, sibling.getFragmentType()); + assertEquals(90, sibling.getStart()); + assertEquals(96, sibling.getEnd()); + + sibling = sibling.getNextSibling(); + assertSame(FragmentType.BEFORE_NAME, sibling.getFragmentType()); + assertEquals(96, sibling.getStart()); + assertEquals(100, sibling.getEnd()); + + sibling = sibling.getNextSibling(); + assertSame(FragmentType.NAME, sibling.getFragmentType()); + assertEquals(100, sibling.getStart()); + assertEquals(111, sibling.getEnd()); + + sibling = sibling.getNextSibling(); + assertSame(FragmentType.AFTER_NAME, sibling.getFragmentType()); + assertEquals(111, sibling.getStart()); + assertEquals(120, sibling.getEnd()); + + sibling = sibling.getNextSibling(); + assertSame(FragmentType.BODY, sibling.getFragmentType()); + assertEquals(120, sibling.getStart()); + assertEquals(131, sibling.getEnd()); + } + + @Test + public void testSourceFragmentAddChild() throws Exception { + //contract: check build of the tree of SourceFragments + SourceFragment rootFragment = createFragment(10, 20); + SourceFragment f; + //add child + assertSame(rootFragment, rootFragment.add(f = createFragment(10, 15))); + assertSame(rootFragment.getFirstChild(), f); + + //add child which is next sibling of first child + assertSame(rootFragment, rootFragment.add(f = createFragment(15, 20))); + assertSame(rootFragment.getFirstChild().getNextSibling(), f); + + //add another child of same start/end, which has to be child of last child + assertSame(rootFragment, rootFragment.add(f = createFragment(15, 20))); + assertSame(rootFragment.getFirstChild().getNextSibling().getFirstChild(), f); + + //add another child of smaller start/end, which has to be child of last child + assertSame(rootFragment, rootFragment.add(f = createFragment(16, 20))); + assertSame(rootFragment.getFirstChild().getNextSibling().getFirstChild().getFirstChild(), f); + + //add next sibling of root element + assertSame(rootFragment, rootFragment.add(f = createFragment(20, 100))); + assertSame(rootFragment.getNextSibling(), f); + + //add prev sibling of root element. We should get new root + f = createFragment(5, 10); + assertSame(f, rootFragment.add(f)); + assertSame(f.getNextSibling(), rootFragment); + } + + @Test + public void testSourceFragmentWrapChild() throws Exception { + //contract: the existing child fragment can be wrapped by a new parent + SourceFragment rootFragment = createFragment(0, 100); + SourceFragment child = createFragment(50, 60); + rootFragment.add(child); + + SourceFragment childWrapper = createFragment(40, 60); + rootFragment.add(childWrapper); + assertSame(rootFragment.getFirstChild(), childWrapper); + assertSame(rootFragment.getFirstChild().getFirstChild(), child); + } + + @Test + public void testLocalizationOfSourceFragment() throws Exception { + SourceFragment rootFragment = createFragment(0, 100); + SourceFragment x; + rootFragment.add(createFragment(50, 60)); + rootFragment.add(createFragment(60, 70)); + rootFragment.add(x = createFragment(50, 55)); + + assertSame(x, rootFragment.getSourceFragmentOf(50, 55)); + assertSame(rootFragment, rootFragment.getSourceFragmentOf(0, 100)); + assertSame(rootFragment.getFirstChild(), rootFragment.getSourceFragmentOf(50, 60)); + assertSame(rootFragment.getFirstChild().getNextSibling(), rootFragment.getSourceFragmentOf(60, 70)); + } + + + private SourceFragment createFragment(int start, int end) { + return new SourceFragment(null, new SourcePositionImpl(null, start, end - 1, null)); + } +} diff --git a/src/test/java/spoon/test/position/testclasses/CastVariableRead.java b/src/test/java/spoon/test/position/testclasses/CastVariableRead.java new file mode 100644 index 00000000000..4eefea6ebf4 --- /dev/null +++ b/src/test/java/spoon/test/position/testclasses/CastVariableRead.java @@ -0,0 +1,11 @@ +package spoon.test.position.testclasses; + +import java.util.ArrayList; +import java.util.List; + +public class CastVariableRead { + void m() { + Object s = new ArrayList<>(); + List x = (List) s; + } +} \ No newline at end of file diff --git a/src/test/java/spoon/test/prettyprinter/PrintChangesTest.java b/src/test/java/spoon/test/prettyprinter/PrintChangesTest.java index 97299d5b9d4..8a8990e67a8 100644 --- a/src/test/java/spoon/test/prettyprinter/PrintChangesTest.java +++ b/src/test/java/spoon/test/prettyprinter/PrintChangesTest.java @@ -8,7 +8,7 @@ import org.junit.Test; import spoon.Launcher; -import spoon.experimental.modelobs.ChangeCollector; +import spoon.experimental.modelobs.SourceFragmentsTreeCreatingChangeCollector; import spoon.reflect.cu.CompilationUnit; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtType; @@ -25,7 +25,7 @@ public void testPrintUnchaged() throws Exception { Factory f = ctClass.getFactory(); - new ChangeCollector().attachTo(f.getEnvironment()); + new SourceFragmentsTreeCreatingChangeCollector().attachTo(f.getEnvironment()); ChangesAwareDefaultJavaPrettyPrinter printer = new ChangesAwareDefaultJavaPrettyPrinter(f.getEnvironment()); CompilationUnit cu = f.CompilationUnit().getOrCreate(ctClass); @@ -48,7 +48,7 @@ public void testPrintChanged() throws Exception { final CtClass ctClass = launcher.getFactory().Class().get(ToBeChanged.class); - new ChangeCollector().attachTo(f.getEnvironment()); + new SourceFragmentsTreeCreatingChangeCollector().attachTo(f.getEnvironment()); //change the model ctClass.getField("string").setSimpleName("modified");