From ee410e4835941dac531a3dd7a859d3942390b922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Vojt=C4=9Bchovsk=C3=BD?= Date: Thu, 26 Jul 2018 10:58:42 +0200 Subject: [PATCH 01/16] feat: detailed tree of SourceFragments of CompilationUnit --- .../spoon/reflect/cu/CompilationUnit.java | 16 + .../change/CollectionSourceFragment.java | 86 ++ .../printer/change/ElementSourceFragment.java | 874 ++++++++++++++++++ .../printer/change/SourceFragment.java | 26 + .../printer/change/TokenSourceFragment.java | 49 + .../visitor/printer/change/TokenType.java | 30 + .../reflect/cu/CompilationUnitImpl.java | 32 +- src/test/java/spoon/MavenLauncherTest.java | 6 +- .../SpoonArchitectureEnforcerTest.java | 1 + src/test/java/spoon/test/main/MainTest.java | 38 +- .../test/position/TestSourceFragment.java | 223 +++++ .../testclasses/FooSourceFragments.java | 32 + .../MethodWithJavaDocAndModifiers.java | 12 + 13 files changed, 1420 insertions(+), 5 deletions(-) create mode 100644 src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java create mode 100644 src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java create mode 100644 src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java create mode 100644 src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java create mode 100644 src/main/java/spoon/reflect/visitor/printer/change/TokenType.java create mode 100644 src/test/java/spoon/test/position/TestSourceFragment.java create mode 100644 src/test/java/spoon/test/position/testclasses/FooSourceFragments.java create mode 100644 src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java diff --git a/src/main/java/spoon/reflect/cu/CompilationUnit.java b/src/main/java/spoon/reflect/cu/CompilationUnit.java index cfd1d14ba35..fed60343a1e 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.ElementSourceFragment; +import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtImport; import spoon.support.Experimental; @@ -165,4 +167,18 @@ enum UNIT_TYPE { @Experimental void setImports(Set imports); + /** + * @return root {@link ElementSourceFragment} of the tree of all the source fragments of origin source code of this compilation unit + * Note: this method creates tree of {@link ElementSourceFragment}s ... it can be a lot of instances + * If the tree of {@link ElementSourceFragment}s is needed then it MUST be created + * BEFORE the Spoon model of this {@link CompilationUnit} is changed. The best creation time is immediately after Spoon model is built. + * Otherwise there might be missing some {@link ElementSourceFragment}s when {@link CtElement}s are deleted + */ + ElementSourceFragment getRootSourceFragment(); + + /** + * @param element the {@link CtElement} whose origin source code is needed. + * @return {@link ElementSourceFragment} which mirrors the origin source code of the `element` or null. + */ + ElementSourceFragment getSourceFragment(SourcePositionHolder element); } diff --git a/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java new file mode 100644 index 00000000000..4aab9201213 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java @@ -0,0 +1,86 @@ +/** + * 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.reflect.visitor.printer.change; + +import java.util.List; + +import spoon.reflect.meta.ContainerKind; +import spoon.reflect.path.CtRole; + +/** + * {@link SourceFragment} of List or Set of {@link ElementSourceFragment}s which belongs to collection role. + * For example list of Type members or list of parameters, etc. + * Or set of modifiers and annotations + */ +public class CollectionSourceFragment implements SourceFragment { + + private final List items; + + public CollectionSourceFragment(List items) { + super(); + this.items = items; + } + + @Override + public String getSourceCode() { + StringBuilder sb = new StringBuilder(); + for (SourceFragment childSourceFragment : items) { + sb.append(childSourceFragment.getSourceCode()); + } + return sb.toString(); + } + + /** + * @return child source fragments of this collection + */ + public List getItems() { + return items; + } + + @Override + public String toString() { + return items.toString(); + } + + /** + * @return true if collection contains only children of one role handler with container kind LIST + */ + public boolean isOrdered() { + CtRole role = null; + ContainerKind kind = null; + for (SourceFragment childSourceFragment : items) { + if (childSourceFragment instanceof ElementSourceFragment) { + ElementSourceFragment esf = (ElementSourceFragment) childSourceFragment; + if (role == null) { + role = esf.getRoleInParent(); + kind = esf.getContainerKindInParent(); + if (kind != ContainerKind.LIST) { + return false; + } + } else { + if (role != esf.getRoleInParent()) { + //the collection contains elements of different roles. It cannot be ordered + return false; + } + //else there is another element of the same role - ok + } + } + } + //there are only elements of one role of container kind LIST + return true; + } +} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java new file mode 100644 index 00000000000..50bcf993d91 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java @@ -0,0 +1,874 @@ +/** + * 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.reflect.visitor.printer.change; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import spoon.SpoonException; +import spoon.reflect.code.CtComment; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.cu.CompilationUnit; +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.cu.SourcePositionHolder; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtModifiable; +import spoon.reflect.meta.ContainerKind; +import spoon.reflect.meta.RoleHandler; +import spoon.reflect.meta.impl.RoleHandlerHelper; +import spoon.reflect.path.CtRole; +import spoon.reflect.visitor.CtScanner; +import spoon.support.reflect.CtExtendedModifier; +import spoon.support.reflect.cu.position.SourcePositionImpl; + +/** + * Represents a part of source code of an {@link CtElement} + * It is connected into a tree of {@link ElementSourceFragment}s. + * That tree can be build by {@link CompilationUnit#getRootSourceFragment()} + * And the tree of {@link ElementSourceFragment}s related to one element can be returned by {@link CompilationUnit#getSourceFragment(SourcePositionHolder)} + */ +public class ElementSourceFragment implements SourceFragment { + + private final SourcePositionHolder element; + private final RoleHandler roleHandlerInParent; + private ElementSourceFragment nextSibling; + private ElementSourceFragment firstChild; + + /** + * Creates a source fragment of {@link SourcePositionHolder} + * + * @param element target {@link SourcePositionHolder} + * @param roleHandlerInParent The {@link RoleHandler}, which defines role of target `element` in it's parent + */ + public ElementSourceFragment(SourcePositionHolder element, RoleHandler roleHandlerInParent) { + this.element = element; + this.roleHandlerInParent = roleHandlerInParent; + } + + /** + * @return offset of first character which belongs to this fragment + */ + public int getStart() { + if (firstChild != null) { + return Math.min(getSourcePosition().getSourceStart(), firstChild.getStart()); + } + return getSourcePosition().getSourceStart(); + } + + /** + * @return offset of character after this fragment + */ + public int getEnd() { + if (firstChild != null) { + return Math.max(getSourcePosition().getSourceEnd() + 1, firstChild.getLastSibling().getEnd()); + } + return getSourcePosition().getSourceEnd() + 1; + } + + /** + * @return {@link SourcePosition} of this fragment + */ + public SourcePosition getSourcePosition() { + return element.getPosition(); + } + + @Override + public String toString() { + return "|" + getStart() + ", " + getEnd() + "|" + getSourceCode() + "|"; + } + + /** + * @return origin source code of this fragment + */ + @Override + public String getSourceCode() { + return getSourceCode(getStart(), getEnd()); + } + + /** + * @param start start offset relative to compilation unit + * @param end end offset (after last character) relative to compilation unit + * @return source code of this Fragment between start/end offsets + */ + public String getSourceCode(int start, int end) { + String src = getOriginalSourceCode(); + if (src != null) { + return src.substring(start, end); + } + return null; + } + + /** + * @return true if position points to same compilation unit (source file) as this SourceFragment + */ + private boolean isFromSameSource(SourcePosition position) { + return getSourcePosition().getCompilationUnit().equals(position.getCompilationUnit()); + } + + /** + * Builds tree of {@link SourcePosition}s of `element` and all it's children + * @param element the root element of the tree + */ + public void addTreeOfSourceFragmentsOfElement(CtElement element) { + SourcePosition sp = element.getPosition(); + Deque parents = new ArrayDeque<>(); + parents.push(this); + //scan all children of `element` and build tree of SourceFragments + new CtScanner() { + CtRole scannedRole; + public void scan(CtRole role, CtElement element) { + scannedRole = role; + super.scan(role, element); + } + @Override + protected void enter(CtElement e) { + ElementSourceFragment newFragment = addChild(parents.peek(), scannedRole, e); + if (newFragment != null) { + parents.push(newFragment); + if (e instanceof CtModifiable) { + CtModifiable modifiable = (CtModifiable) e; + Set modifiers = modifiable.getExtendedModifiers(); + for (CtExtendedModifier ctExtendedModifier : modifiers) { + addChild(newFragment, CtRole.MODIFIER, ctExtendedModifier); + } + } + } + } + @Override + protected void exit(CtElement e) { + ElementSourceFragment topFragment = parents.peek(); + if (topFragment != null && topFragment.getElement() == e) { + parents.pop(); + } + } + }.scan(element.getRoleInParent(), element); + } + /** + * @param parentFragment the parent {@link ElementSourceFragment}, which will receive {@link ElementSourceFragment} made for `otherElement` + * @param roleInParent the {@link CtRole} of `otherElement` in scope of element of `parentFragment` + * @param otherElement {@link SourcePositionHolder} whose {@link ElementSourceFragment} has to be added to `parentFragment` + * @return new {@link ElementSourceFragment} created for `otherElement` or null if `otherElement` has no source position or doesn't belong to the same compilation unit + */ + private ElementSourceFragment addChild(ElementSourceFragment parentFragment, CtRole roleInParent, SourcePositionHolder otherElement) { + SourcePosition otherSourcePosition = otherElement.getPosition(); + if (otherSourcePosition instanceof SourcePositionImpl && otherSourcePosition.getCompilationUnit() != null) { + SourcePositionImpl childSPI = (SourcePositionImpl) otherSourcePosition; + if (parentFragment.isFromSameSource(otherSourcePosition)) { + ElementSourceFragment otherFragment = new ElementSourceFragment(otherElement, parentFragment.getRoleHandler(roleInParent, otherElement)); + //parent and child are from the same file. So we can connect their positions into one tree + CMP cmp = parentFragment.compare(otherFragment); + if (cmp == CMP.OTHER_IS_CHILD) { + //child belongs under parent - OK + parentFragment.addChild(otherFragment); + return otherFragment; + } else { + if (cmp == CMP.OTHER_IS_AFTER || cmp == CMP.OTHER_IS_BEFORE) { + if (otherElement instanceof CtComment) { + /* + * comments of elements are sometime not included in source position of element. + * because comments are ignored tokens for java compiler, which computes start/end of elements + * Example: + * + * //a comment + * aStatement(); + * + */ + if (otherFragment.getStart() == 0) { + //it is CompilationUnit comment, which is before package and imports, so it doesn't belong to class + //No problem. Simply add comment at correct position into SourceFragment tree, starting from ROOT + addChild(otherFragment); + return otherFragment; + } + //add this child into parent's source fragment and extend that parent source fragment + parentFragment.addChild(otherFragment); + return otherFragment; + } + } + //the source position of child element is not included in source position of parent element + //I (Pavel) am not sure how to handle it, so let's wait until it happens... +// 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 ... + throw new SpoonException("The SourcePosition of elements are not consistent\nparentFragment: " + parentFragment + "\notherFragment: " + otherFragment); + } + } else { + throw new SpoonException("SourcePosition from unexpected compilation unit: " + otherSourcePosition + " expected is: " + parentFragment.getSourcePosition()); + } + } + //do not connect that undefined source position + return null; + } + + private RoleHandler getRoleHandler(CtRole roleInParent, SourcePositionHolder otherElement) { + SourcePositionHolder parent = element; + if (parent instanceof CompilationUnit) { + parent = null; + } + if (parent == null) { + if (otherElement instanceof CtElement) { + parent = ((CtElement) otherElement).getParent(); + } + } + if (parent instanceof CtElement) { + CtElement ele = (CtElement) parent; + return RoleHandlerHelper.getRoleHandler(ele.getClass(), roleInParent); + } + return null; + } + + /** + * adds `other` {@link ElementSourceFragment} into tree of {@link ElementSourceFragment}s represented by this root element + * + * @param other to be added {@link ElementSourceFragment} + * @return new root of the tree of the {@link ElementSourceFragment}s. It can be be this or `other` + */ + public ElementSourceFragment add(ElementSourceFragment 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, merge this and all siblings of `this` as children and siblings of `other` + other.merge(this); + return other; + } + throw new SpoonException("Unexpected compare result: " + cmp); + } + + private void merge(ElementSourceFragment tobeMerged) { + while (tobeMerged != null) { + ElementSourceFragment nextTobeMerged = tobeMerged.getNextSibling(); + //disconnect tobeMerged from nextSiblings before we add it. So it is added individually and not with wrong siblings too + tobeMerged.nextSibling = null; + add(tobeMerged); + tobeMerged = nextTobeMerged; + } + } + + /** + * adds `fragment` as child fragment of this fragment. If child is located before or after this fragment, + * then start/end of this fragment is moved + * @param fragment to be add + */ + public void addChild(ElementSourceFragment fragment) { + if (firstChild == null) { + firstChild = fragment; + } else { + firstChild = firstChild.add(fragment); + } + } + + private void addNextSibling(ElementSourceFragment sibling) { + if (nextSibling == null) { + nextSibling = sibling; + } else { + nextSibling = nextSibling.add(sibling); + } + } + + private ElementSourceFragment getLastSibling() { + ElementSourceFragment lastSibling = this; + while (lastSibling.nextSibling != null) { + lastSibling = lastSibling.nextSibling; + } + return lastSibling; + } + + 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(ElementSourceFragment 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() + "\"]"); + } + + /** + * @return {@link ElementSourceFragment} which belongs to the same parent and is next in the sources + */ + public ElementSourceFragment getNextSibling() { + return nextSibling; + } + + /** + * @return {@link ElementSourceFragment}, which is first child of this fragment + */ + public ElementSourceFragment getFirstChild() { + return firstChild; + } + + /** + * Searches the tree of fragments for the {@link ElementSourceFragment} with expected `element`, + * which contains `start` and `end` source interval. + * It searches in siblings and children of this {@link ElementSourceFragment} recursively. + * @param element the {@link SourcePositionHolder} of fragment it is looking for or null for any element + * @param start the start offset of searched fragment + * @param end the offset of next character after the end of searched fragment + * + * @return {@link ElementSourceFragment} which represents the root of the CtElement whose sources are in interval [start, end] + */ + public ElementSourceFragment getSourceFragmentOf(SourcePositionHolder element, int start, int end) { + int myEnd = getEnd(); + if (myEnd <= start) { + //search in next sibling + if (nextSibling == null) { + return null; + } + return getRootFragmentOfElement(nextSibling.getSourceFragmentOf(element, start, end)); + } + int myStart = getStart(); + if (myStart <= start) { + if (myEnd >= end) { + if (myStart == start && myEnd == end) { + //we have found exact match + if (element != null && getElement() != element) { + if (firstChild == null) { + throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); + } + return firstChild.getSourceFragmentOf(element, start, end); + } + return this; + } + //it is the child + if (firstChild == null) { + if (element != null && getElement() != element) { + throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); + } + return this; + } + ElementSourceFragment child = getRootFragmentOfElement(firstChild.getSourceFragmentOf(element, start, end)); + if (child != null) { + //all children are smaller then this element + return child; + } + //so this fragment is last one which wraps whole element + if (element != null && getElement() != element) { + throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); + } + return this; + } + //start - end overlaps over multiple fragments + throw new SpoonException("Invalid start/end interval. It overlaps multiple fragments."); + } + return null; + } + + private ElementSourceFragment getRootFragmentOfElement(ElementSourceFragment 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. + * May be null + */ + public SourcePositionHolder getElement() { + return element; + } + + /** + * Note: the List of children is flat. The child fragments of collections (parameters, type members, ...) are next to each other. + * @return list of child fragments of this {@link ElementSourceFragment}. + */ + public List getChildrenFragments() { + if (element instanceof CtLiteral) { + return Collections.singletonList(new TokenSourceFragment(getSourceCode(), TokenType.LITERAL)); + } + List children = new ArrayList<>(); + int off = getStart(); + ElementSourceFragment child = getFirstChild(); + while (child != null) { + forEachConstantFragment(off, child.getStart(), cf -> children.add(cf)); + children.add(child); + off = child.getEnd(); + child = child.getNextSibling(); + } + forEachConstantFragment(off, getEnd(), cf -> children.add(cf)); + return children; + } + + /** + * Detects all child fragments of this {@link ElementSourceFragment}. + * Note: the List of children contains one {@link CollectionSourceFragment} for each collection of fragments (parameters, type members, ...). + * Note: the {@link CollectionSourceFragment} may contain a mix of fragments of different roles, when they overlap. + * For example this code contains mix of annotations and modifiers + * public @Deprecated static @Ignored void method() + * @return list of child fragments of this {@link ElementSourceFragment} where fragments, + * which belongs to the same collection are grouped into {@link CollectionSourceFragment} + */ + public List getGroupedChildrenFragments() { + List flatChildren = getChildrenFragments(); + List result = new ArrayList<>(); + int i = 0; + while (i < flatChildren.size()) { + SourceFragment child = flatChildren.get(i); + if (child instanceof TokenSourceFragment) { + result.add(child); + i++; + continue; + } else if (child instanceof ElementSourceFragment) { + ElementSourceFragment esf = (ElementSourceFragment) child; + ContainerKind kind = esf.getContainerKindInParent(); + if (kind == ContainerKind.SINGLE) { + //it is root element or there is always only one child instance in parent + result.add(child); + i++; + continue; + } + //there can be 0, 1 or more items of children of the same role + //search for another element of the same role + Set foundRoles = new HashSet<>(); + foundRoles.add(checkNotNull(esf.getRoleInParent())); + List childrenInSameCollection = new ArrayList<>(); + //but first include prefix whitespace + SourceFragment spaceChild = removeSuffixSpace(result); + if (spaceChild != null) { + childrenInSameCollection.add(spaceChild); + } + childrenInSameCollection.add(esf); + int lastOfSameRole = findIndexOfLastChildTokenOfRoleHandler(flatChildren, i, esf.getRoleInParent()); + //search for other roles in that interval + i++; + while (i <= lastOfSameRole) { + child = flatChildren.get(i); + childrenInSameCollection.add(child); + CtRole role = null; + if (child instanceof ElementSourceFragment) { + ElementSourceFragment esf2 = (ElementSourceFragment) child; + role = esf2.getRoleInParent(); + } + if (role != null && role != CtRole.COMMENT && foundRoles.add(role)) { + //there is another role in same block, search for last one + lastOfSameRole = Math.max(lastOfSameRole, findIndexOfLastChildTokenOfRoleHandler(flatChildren, i + 1, role)); + } + i++; + } + //add suffix space + if (i < flatChildren.size()) { + SourceFragment nextChild = flatChildren.get(i); + if (isSpaceFragment(nextChild)) { + childrenInSameCollection.add(nextChild); + i++; + } + } + result.add(new CollectionSourceFragment(childrenInSameCollection)); + } else { + throw new SpoonException("Unexpected SourceFragment of type " + child.getClass()); + } + } + return result; + } + + private SourceFragment removeSuffixSpace(List list) { + if (list.size() > 0) { + SourceFragment lastChild = list.get(list.size() - 1); + if (isSpaceFragment(lastChild)) { + list.remove(list.size() - 1); + return lastChild; + } + } + return null; + } + + private T checkNotNull(T o) { + if (o == null) { + throw new SpoonException("Unexpected null value"); + } + return o; + } + + private static int findIndexOfLastChildTokenOfRoleHandler(List childFragments, int start, CtRole role) { + return findIndexOfPreviousFragment(childFragments, start, + filter(ElementSourceFragment.class, fragment -> fragment.getRoleInParent() == role)); + } + + private enum CharType { + SPACE, + NON_SPACE; + + static CharType fromChar(char c) { + return Character.isWhitespace(c) ? SPACE : NON_SPACE; + } + } + + private static final Set separators = new HashSet<>(Arrays.asList("->", "::", "...")); + static { + "(){}[];,.:@=<>?&|".chars().forEach(c -> separators.add(new String(Character.toChars(c)))); + } + private static final Set operators = new HashSet<>(Arrays.asList( + "=", + ">", + "<", + "!", + "~", + "?", + ":", + "==", + "<=", + ">=", + "!=", + "&&", + "||", + "++", + "--", + "+", + "-", + "*", + "/", + "&", + "|", + "^", + "%", + "<<", ">>", ">>>", + + "+=", + "-=", + "*=", + "/=", + "&=", + "|=", + "^=", + "%=", + "<<=", + ">>=", + ">>>="/*, + it is handled as keyword here + "instanceof" + */ + )); + + private static final String[] javaKeywordsJoined = new String[] { + "abstract continue for new switch", + "assert default goto package synchronized", + "boolean do if private this", + "break double implements protected throw", + "byte else import public throws", + "case enum instanceof return transient", + "catch extends int short try", + "char final interface static void", + "class finally long strictfp volatile", + "const float native super while"}; + + private static final Set javaKeywords = new HashSet<>(); + static { + for (String str : javaKeywordsJoined) { + StringTokenizer st = new StringTokenizer(str, " "); + while (st.hasMoreTokens()) { + javaKeywords.add(st.nextToken()); + } + } + } + + private static final List matchers = new ArrayList<>(); + static { + separators.forEach(s -> matchers.add(new StringMatcher(s, TokenType.SEPARATOR))); + operators.forEach(s -> matchers.add(new StringMatcher(s, TokenType.OPERATOR))); + } + + /** + * Calls `consumer` once for each constant {@link SourceFragment} found in source code between `start` and `end` + */ + private void forEachConstantFragment(int start, int end, Consumer consumer) { + if (start == end) { + return; + } + if (start > end) { + throw new SpoonException("Inconsistent start/end. Start=" + start + " is greater then End=" + end); + } + String sourceCode = getOriginalSourceCode(); + StringBuffer buff = new StringBuffer(); + CharType lastType = null; + int off = start; + while (off < end) { + char c = sourceCode.charAt(off); + CharType type = CharType.fromChar(c); + if (type != lastType) { + if (lastType != null) { + onCharSequence(lastType, buff, consumer); + buff.setLength(0); + } + lastType = type; + } + buff.append(c); + off++; + } + onCharSequence(lastType, buff, consumer); + } + + private void onCharSequence(CharType type, StringBuffer buff, Consumer consumer) { + if (type == CharType.SPACE) { + consumer.accept(new TokenSourceFragment(buff.toString(), TokenType.SPACE)); + return; + } + char[] str = new char[buff.length()]; + buff.getChars(0, buff.length(), str, 0); + int off = 0; + while (off < str.length) { + //detect java identifier or keyword + int lenOfIdentifier = detectJavaIdentifier(str, off); + if (lenOfIdentifier > 0) { + String identifier = new String(str, off, lenOfIdentifier); + if (javaKeywords.contains(identifier)) { + //it is a java keyword + consumer.accept(new TokenSourceFragment(identifier, TokenType.KEYWORD)); + } else { + //it is a java identifier + consumer.accept(new TokenSourceFragment(identifier, TokenType.IDENTIFIER)); + } + off += lenOfIdentifier; + continue; + } + //detect longest match in matchers + StringMatcher longestMatcher = null; + for (StringMatcher strMatcher : matchers) { + if (strMatcher.isMatch(str, off)) { + longestMatcher = strMatcher.getLonger(longestMatcher); + } + } + if (longestMatcher == null) { + throw new SpoonException("Unexpected source text: " + buff.toString()); + } + consumer.accept(new TokenSourceFragment(longestMatcher.toString(), longestMatcher.getType())); + off += longestMatcher.getLength(); + } + } + + /** + * @return number of characters in buff starting from start which are java identifier + */ + private int detectJavaIdentifier(char[] buff, int start) { + int len = buff.length; + int o = start; + if (start <= len) { + char c = buff[o]; + if (Character.isJavaIdentifierStart(c)) { + o++; + while (o < len) { + c = buff[o]; + if (Character.isJavaIdentifierPart(c) == false) { + break; + } + o++; + } + } + } + return o - start; + } + + private String getOriginalSourceCode() { + CompilationUnit cu = getSourcePosition().getCompilationUnit(); + if (cu != null) { + return cu.getOriginalSourceCode(); + } + return null; + } + + private static final class StringMatcher { + private final TokenType type; + private final char[] chars; + + private StringMatcher(final String str, TokenType type) { + super(); + this.type = type; + chars = str.toCharArray(); + } + + public boolean isMatch(final char[] buffer, int pos) { + final int len = chars.length; + if (pos + len > buffer.length) { + return false; + } + for (int i = 0; i < chars.length; i++, pos++) { + if (chars[i] != buffer[pos]) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return new String(chars); + } + + public int getLength() { + return chars.length; + } + + public StringMatcher getLonger(StringMatcher m) { + if (m != null && m.getLength() > getLength()) { + return m; + } + return this; + } + + public TokenType getType() { + return type; + } + } + + /** + * @return role of the element of this fragment in scope of it's parent + */ + public CtRole getRoleInParent() { + return roleHandlerInParent != null ? roleHandlerInParent.getRole() : null; + } + + /** + * @return the {@link ContainerKind} of the attribute which holds the element of this fragment in it's parent + */ + public ContainerKind getContainerKindInParent() { + if (roleHandlerInParent != null) { + if (roleHandlerInParent.getRole() != CtRole.COMMENT) { + return roleHandlerInParent.getContainerKind(); + } + } + return ContainerKind.SINGLE; + } + /** + * looks for next fragment whose {@link Predicate} `test` returns true + * @param start - the index of first to be checked fragment + * @return index of found fragment, or -1 if not found + */ + static int findIndexOfNextFragment(List fragments, int start, Predicate test) { + while (start < fragments.size()) { + SourceFragment fragment = fragments.get(start); + if (test.test(fragment)) { + return start; + } + start++; + } + return -1; + } + + /** + * @param start the index of element with lower index which is checked and may be returned + * @param test a {@link Predicate}, which is evaluated for each item of `fragments` starting from last one and ending with item in index `start` + * @return index of found fragment, or -1 if not found + */ + static int findIndexOfPreviousFragment(List fragments, int start, Predicate test) { + int i = fragments.size() - 1; + while (i >= start) { + if (test.test(fragments.get(i))) { + return i; + } + i--; + } + return -1; + } + + /** + * @param predicate the {@link Predicate}, which has to be checkd for each item of {@link CollectionSourceFragment} + * @return {@link Predicate} which calls `predicate` for each item of {@link CollectionSourceFragment} + * Returned {@link Predicate} returns true only if `predicate` returns true on at least one item + */ + static Predicate checkCollectionItems(Predicate predicate) { + return (SourceFragment fragment) -> { + if (fragment instanceof CollectionSourceFragment) { + CollectionSourceFragment collectionFragment = (CollectionSourceFragment) fragment; + for (SourceFragment itemFragment : collectionFragment.getItems()) { + if (predicate.test(itemFragment)) { + return true; + } + } + return false; + } else { + return predicate.test(fragment); + } + }; + } + + /** + * @param predicate to be called {@link Predicate} + * @return {@link Predicate} which calls `predicate` only for {@link SourceFragment}s of of type `clazz` and returns false for others + */ + static Predicate filter(Class clazz, Predicate predicate) { + return fragment -> { + if (clazz.isInstance(fragment)) { + return predicate.test((T) fragment); + } + return false; + }; + } + + /** + * @return true if {@link SourceFragment} represents a white space + */ + static boolean isSpaceFragment(SourceFragment fragment) { + return fragment instanceof TokenSourceFragment && ((TokenSourceFragment) fragment).getType() == TokenType.SPACE; + } +} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java new file mode 100644 index 00000000000..01ef9487a6e --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java @@ -0,0 +1,26 @@ +/** + * 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.reflect.visitor.printer.change; + +/** + */ +public interface SourceFragment { + /** + * @return origin source code of whole fragment represented by this instance + */ + String getSourceCode(); +} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java new file mode 100644 index 00000000000..1978d926af8 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java @@ -0,0 +1,49 @@ +/** + * 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.reflect.visitor.printer.change; + +/** + * a {@link SourceFragment} of some primitive String token. + */ +public class TokenSourceFragment implements SourceFragment { + + private final String source; + private final TokenType type; + + public TokenSourceFragment(String source, TokenType type) { + super(); + this.source = source; + this.type = type; + } + + @Override + public String getSourceCode() { + return source; + } + + /** + * @return type of token of this fragment + */ + public TokenType getType() { + return type; + } + + @Override + public String toString() { + return "|" + getSourceCode() + "|"; + } +} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java b/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java new file mode 100644 index 00000000000..2a4b0462362 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java @@ -0,0 +1,30 @@ +package spoon.reflect.visitor.printer.change; + +enum TokenType { + + SEPARATOR(false, false), + OPERATOR(false, false), + LITERAL(false, false), + KEYWORD(false, false), + IDENTIFIER(false, false), + CODE_SNIPPET(false, false), + COMMENT(false, false), + NEW_LINE(true, false), + INC_TAB(true, true), + DEC_TAB(true, true), + SPACE(true, false); + + private final boolean whiteSpace; + private final boolean tab; + + TokenType(boolean whiteSpace, boolean tab) { + this.whiteSpace = whiteSpace; + this.tab = tab; + } + boolean isWhiteSpace() { + return whiteSpace; + } + public boolean isTab() { + return tab; + } +} diff --git a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java index 833b6117440..f7e35a816f4 100644 --- a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java +++ b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java @@ -16,16 +16,19 @@ */ 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.cu.SourcePositionHolder; +import spoon.reflect.declaration.CtImport; import spoon.reflect.declaration.CtModule; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; import spoon.reflect.factory.Factory; -import spoon.reflect.declaration.CtImport; import spoon.reflect.visitor.DefaultJavaPrettyPrinter; import spoon.reflect.visitor.filter.TypeFilter; +import spoon.reflect.visitor.printer.change.ElementSourceFragment; import spoon.support.reflect.cu.position.PartialSourcePositionImpl; import java.io.File; @@ -65,6 +68,8 @@ public class CompilationUnitImpl implements CompilationUnit, FactoryAccessor { */ private int[] lineSeparatorPositions; + private ElementSourceFragment rootFragment; + @Override public UNIT_TYPE getUnitType() { // we try to guess based on the file name @@ -301,6 +306,31 @@ public SourcePosition getOrCreatePartialSourcePosition() { return myPartialSourcePosition; } + @Override + public ElementSourceFragment getRootSourceFragment() { + if (rootFragment == null) { + String originSourceCode = getOriginalSourceCode(); + rootFragment = new ElementSourceFragment(this, null); + for (CtImport imprt : getImports()) { + rootFragment.addChild(new ElementSourceFragment(imprt, null /*TODO role for import of CU*/)); + } + for (CtType ctType : declaredTypes) { + rootFragment.addTreeOfSourceFragmentsOfElement(ctType); + } + } + return rootFragment; + } + + @Override + public ElementSourceFragment getSourceFragment(SourcePositionHolder element) { + ElementSourceFragment rootFragment = getRootSourceFragment(); + SourcePosition sp = element.getPosition(); + if (sp.getCompilationUnit() != this) { + throw new SpoonException("Cannot return SourceFragment of element for different CompilationUnit"); + } + return rootFragment.getSourceFragmentOf(element, sp.getSourceStart(), sp.getSourceEnd() + 1); + } + @Override public int[] getLineSeparatorPositions() { return lineSeparatorPositions; diff --git a/src/test/java/spoon/MavenLauncherTest.java b/src/test/java/spoon/MavenLauncherTest.java index f3f1b66aacf..66136f31dc6 100644 --- a/src/test/java/spoon/MavenLauncherTest.java +++ b/src/test/java/spoon/MavenLauncherTest.java @@ -47,15 +47,15 @@ public void spoonMavenLauncherTest() { assertEquals(190, launcher.getEnvironment().getSourceClasspath().length); - // 56 because of the sub folders of src/main/java - assertEquals(59, launcher.getModelBuilder().getInputSources().size()); + // number of the sub folders of src/main/java + assertEquals(60, launcher.getModelBuilder().getInputSources().size()); // with the tests launcher = new MavenLauncher("./", MavenLauncher.SOURCE_TYPE.ALL_SOURCE); assertEquals(195, launcher.getEnvironment().getSourceClasspath().length); - // 236 because of the sub folders of src/main/java and src/test/java + // number of the sub folders of src/main/java and src/test/java assertTrue("size: " + launcher.getModelBuilder().getInputSources().size(), launcher.getModelBuilder().getInputSources().size() >= 220); // specify the pom.xml diff --git a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java index 6fd64495e94..f557250bea4 100644 --- a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java +++ b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java @@ -365,6 +365,7 @@ public void testSpecPackage() { officialPackages.add("spoon.reflect.visitor.chain"); officialPackages.add("spoon.reflect.visitor.filter"); officialPackages.add("spoon.reflect.visitor.printer"); + officialPackages.add("spoon.reflect.visitor.printer.change"); officialPackages.add("spoon.reflect.visitor"); officialPackages.add("spoon.reflect"); officialPackages.add("spoon.support.comparator"); diff --git a/src/test/java/spoon/test/main/MainTest.java b/src/test/java/spoon/test/main/MainTest.java index dfbfca6fe01..bd45143f120 100644 --- a/src/test/java/spoon/test/main/MainTest.java +++ b/src/test/java/spoon/test/main/MainTest.java @@ -40,6 +40,7 @@ import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.PrinterHelper; import spoon.reflect.visitor.filter.TypeFilter; +import spoon.reflect.visitor.printer.change.ElementSourceFragment; import spoon.support.reflect.CtExtendedModifier; import spoon.test.parent.ParentTest; @@ -53,6 +54,7 @@ import java.util.Deque; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -95,7 +97,8 @@ public static void loadModel() { "--source-classpath", systemClassPath, "--compile", // compiling Spoon code itself on the fly "--compliance", "8", - "--level", "OFF" + "--level", "OFF", + "--enable-comments" }); launcher.buildModel(); @@ -471,6 +474,39 @@ public void scan(CtRole role, CtElement element) { }); } + @Test + public void testSourcePositionTreeIsCorrectlyOrdered() { + List types = rootPackage.filterChildren(new TypeFilter<>(CtType.class)).filterChildren((CtType t) -> t.isTopLevel()).list(); + int totalCount = 0; + boolean hasComment = false; + for (CtType type : types) { + SourcePosition sp = type.getPosition(); + totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getRootSourceFragment()); + hasComment = hasComment || type.getComments().size() > 0; + }; + assertTrue(totalCount > 1000); + assertTrue(hasComment); + } + + /** + * Asserts that all siblings and children of sp are well ordered + * @param sourceFragment + * @return number of checked {@link SourcePosition} nodes + */ + private int assertSourcePositionTreeIsCorrectlyOrder(ElementSourceFragment sourceFragment) { + int nr = 0; + int pos = 0; + while (sourceFragment != null) { + nr++; + assertTrue(pos <= sourceFragment.getStart()); + assertTrue(sourceFragment.getStart() <= sourceFragment.getEnd()); + pos = sourceFragment.getEnd(); + nr += assertSourcePositionTreeIsCorrectlyOrder(sourceFragment.getFirstChild()); + sourceFragment = sourceFragment.getNextSibling(); + } + return nr; + } + @Test public void testElementToPathToElementEquivalency() { diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java new file mode 100644 index 00000000000..f8863053e8e --- /dev/null +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -0,0 +1,223 @@ +package spoon.test.position; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Test; + +import spoon.Launcher; +import spoon.SpoonModelBuilder; +import spoon.compiler.SpoonResourceHelper; +import spoon.reflect.code.CtAssignment; +import spoon.reflect.cu.CompilationUnit; +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtType; +import spoon.reflect.factory.Factory; +import spoon.reflect.visitor.printer.change.SourceFragment; +import spoon.reflect.visitor.printer.change.CollectionSourceFragment; +import spoon.reflect.visitor.printer.change.ElementSourceFragment; +import spoon.support.reflect.cu.CompilationUnitImpl; +import spoon.support.reflect.cu.position.SourcePositionImpl; +import spoon.test.position.testclasses.FooSourceFragments; + +public class TestSourceFragment { + + @Test + public void testSourcePositionFragment() throws Exception { + SourcePosition sp = new SourcePositionImpl(DUMMY_COMPILATION_UNIT, 10, 20, null); + ElementSourceFragment sf = new ElementSourceFragment(() -> sp, null); + assertEquals(10, sf.getStart()); + assertEquals(21, sf.getEnd()); + assertSame(sp, sf.getSourcePosition()); + assertNull(sf.getFirstChild()); + assertNull(sf.getNextSibling()); + + } + + @Test + public void testSourceFragmentAddChild() throws Exception { + //contract: check build of the tree of SourceFragments + ElementSourceFragment rootFragment = createFragment(10, 20); + ElementSourceFragment 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 testSourceFragmentAddChildBeforeOrAfter() throws Exception { + //contract: start / end of root fragment is moved when child is added + ElementSourceFragment rootFragment = createFragment(10, 20); + rootFragment.addChild(createFragment(5, 7)); + assertEquals(5, rootFragment.getStart()); + assertEquals(20, rootFragment.getEnd()); + rootFragment.addChild(createFragment(20, 25)); + assertEquals(5, rootFragment.getStart()); + assertEquals(25, rootFragment.getEnd()); + } + + @Test + public void testSourceFragmentWrapChild() throws Exception { + //contract: the existing child fragment can be wrapped by a new parent + ElementSourceFragment rootFragment = createFragment(0, 100); + ElementSourceFragment child = createFragment(50, 60); + rootFragment.add(child); + + ElementSourceFragment childWrapper = createFragment(40, 60); + rootFragment.add(childWrapper); + assertSame(rootFragment.getFirstChild(), childWrapper); + assertSame(rootFragment.getFirstChild().getFirstChild(), child); + } + @Test + public void testSourceFragmentWrapChildrenAndSiblings() throws Exception { + //contract: the two SourceFragment trees merge correctly together + ElementSourceFragment rootFragment = createFragment(0, 100); + ElementSourceFragment child = createFragment(50, 60); + rootFragment.add(child); + + ElementSourceFragment childWrapper = createFragment(40, 70); + ElementSourceFragment childA = createFragment(40, 50); + ElementSourceFragment childB = createFragment(50, 55); + ElementSourceFragment childC = createFragment(60, 65); + ElementSourceFragment childD = createFragment(65, 70); + //add all children and check the root is still childWrapper + assertSame(childWrapper, childWrapper.add(childA).add(childB).add(childC).add(childD)); + //add childWrapper which has to merge with before added child, because childWrapper is parent of child + rootFragment.add(childWrapper); + assertSame(rootFragment.getFirstChild(), childWrapper); + assertSame(childA, childWrapper.getFirstChild()); + assertSame(child, childA.getNextSibling()); + assertSame(childB, child.getFirstChild()); + assertSame(childC, child.getNextSibling()); + assertSame(childD, childC.getNextSibling()); + } + + @Test + public void testElementSourceFragment_getSourceFragmentOf() throws Exception { + //contract: ElementSourceFragment#getSourceFragmentOf returns expected results + ElementSourceFragment rootFragment = createFragment(0, 100); + ElementSourceFragment x; + rootFragment.add(createFragment(50, 60)); + rootFragment.add(createFragment(60, 70)); + rootFragment.add(x = createFragment(50, 55)); + + assertSame(x, rootFragment.getSourceFragmentOf(null, 50, 55)); + assertSame(rootFragment, rootFragment.getSourceFragmentOf(null, 0, 100)); + assertSame(rootFragment.getFirstChild(), rootFragment.getSourceFragmentOf(null, 50, 60)); + assertSame(rootFragment.getFirstChild().getNextSibling(), rootFragment.getSourceFragmentOf(null, 60, 70)); + } + + private static final CompilationUnit DUMMY_COMPILATION_UNIT = new CompilationUnitImpl(); + + private ElementSourceFragment createFragment(int start, int end) { + SourcePosition sp = new SourcePositionImpl(DUMMY_COMPILATION_UNIT, start, end - 1, null); + return new ElementSourceFragment(() -> sp, null); + } + + @Test + public void testSourceFragmentOfIf() throws Exception { + //contract: SourceFragments of some tricky sources are as expected + final Launcher launcher = new Launcher(); + launcher.getEnvironment().setNoClasspath(false); + launcher.getEnvironment().setCommentEnabled(true); + SpoonModelBuilder comp = launcher.createCompiler(); + comp.addInputSources(SpoonResourceHelper.resources("./src/test/java/" + FooSourceFragments.class.getName().replace('.', '/') + ".java")); + comp.build(); + Factory f = comp.getFactory(); + + final CtType foo = f.Type().get(FooSourceFragments.class); + checkElementFragments(foo.getMethodsByName("m1").get(0).getBody().getStatement(0), + "if", "(", "x > 0", ")", "{this.getClass();}", "else", "{/*empty*/}"); + checkElementFragments(foo.getMethodsByName("m2").get(0).getBody().getStatement(0), + "/*c0*/", " ", "if", " ", "/*c1*/", "\t", "(", " ", "//c2", "\n\t\t\t\t", "x > 0", " ", "/*c3*/", " ", ")", " ", "/*c4*/", " ", "{ \n" + + " this.getClass();\n" + + " }", " ", "/*c5*/ else /*c6*/ {\n" + + " /*empty*/\n" + + " }", " ", "/*c7*/"); + checkElementFragments(foo.getMethodsByName("m3").get(0), + "/**\n" + + " * c0\n" + + " */", + group("\n\t", "public", "\n\t", "@Deprecated", " ", "//c1 ends with tab and space\t ", "\n\t", "static", " "), "/*c2*/", " ", + "<", group("T", ",", " ", "U"), ">", + " ", "T", " ", "m3", "(", group("U param", ",", " ", "@Deprecated int p2"), ")", " ", "{\n" + + " return null;\n" + + " }"); + //TODO uncomment and fix source code of element label + //checkElementFragments(foo.getMethodsByName("m4").get(0).getBody().getStatement(0),""); + + checkElementFragments(foo.getMethodsByName("m5").get(0).getBody().getStatement(0),"f", " ", "=", " ", "7.2", ";"); + checkElementFragments(((CtAssignment)foo.getMethodsByName("m5").get(0).getBody().getStatement(0)).getAssignment(),"7.2"); + + } + + private void checkElementFragments(CtElement ele, Object... expectedFragments) { + ElementSourceFragment fragment = ele.getPosition().getCompilationUnit().getSourceFragment(ele); + List children = fragment.getChildrenFragments(); + assertEquals(expandGroup(new ArrayList<>(), expectedFragments), childSourceFragmentsToStrings(children)); + assertGroupsEqual(expectedFragments, fragment.getGroupedChildrenFragments()); + } + + private String[] group(String ...str) { + return str; + } + + private List expandGroup(List result, Object[] items) { + for (Object object : items) { + if (object instanceof String[]) { + String[] strings = (String[]) object; + expandGroup(result, strings); + } else { + result.add((String) object); + } + } + return result; + } + + private static void assertGroupsEqual(Object[] expectedFragments, List groupedChildrenFragments) { + assertEquals(Arrays.stream(expectedFragments).map(item->{ + if (item instanceof String[]) { + return "group("+Arrays.asList((String[]) item).toString() + ")"; + } + return item; + }).collect(Collectors.toList()), groupedChildrenFragments.stream().map(item -> { + if (item instanceof CollectionSourceFragment) { + CollectionSourceFragment csf = (CollectionSourceFragment) item; + return "group("+childSourceFragmentsToStrings(csf.getItems()).toString() + ")"; + } + return item.getSourceCode(); + }).collect(Collectors.toList())); + } + + private static List childSourceFragmentsToStrings(List csf) { + return csf.stream().map(SourceFragment::getSourceCode).collect(Collectors.toList()); + } +} diff --git a/src/test/java/spoon/test/position/testclasses/FooSourceFragments.java b/src/test/java/spoon/test/position/testclasses/FooSourceFragments.java new file mode 100644 index 00000000000..5b3deb9cdf4 --- /dev/null +++ b/src/test/java/spoon/test/position/testclasses/FooSourceFragments.java @@ -0,0 +1,32 @@ +package spoon.test.position.testclasses; + +public class FooSourceFragments { + void m1(int x) { + if(x > 0){this.getClass();}else{/*empty*/} + } + void m2(int x) { + /*c0*/ if /*c1*/ ( //c2 + x > 0 /*c3*/ ) /*c4*/ { + this.getClass(); + } /*c5*/ else /*c6*/ { + /*empty*/ + } /*c7*/ + } + /** + * c0 + */ + public + @Deprecated //c1 ends with tab and space + static /*c2*/ T m3(U param, @Deprecated int p2) { + return null; + } + + void m4() { + label: while(true); + } + + void m5(double f) { + f = 7.2; + } + +} \ No newline at end of file diff --git a/src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java b/src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java new file mode 100644 index 00000000000..56c9dec7c9f --- /dev/null +++ b/src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java @@ -0,0 +1,12 @@ +package spoon.test.position.testclasses; + +public class MethodWithJavaDocAndModifiers { + + /** + * Method with javadoc + * @param parm1 the parameter + */ + public @Deprecated int mWithDoc(int parm1) { + return parm1; + } +} \ No newline at end of file From 08efff0cdc8bc023e39d035909c55d81c5a6312c Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Tue, 21 Aug 2018 19:54:09 +0200 Subject: [PATCH 02/16] TokenType is public --- .../spoon/reflect/visitor/printer/change/TokenType.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java b/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java index 2a4b0462362..d0b9f3b48d7 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java @@ -1,6 +1,12 @@ package spoon.reflect.visitor.printer.change; -enum TokenType { +import spoon.reflect.visitor.TokenWriter; + +/** + * Type of {@link TokenSourceFragment} token. + * Note: These types mirrors the methods of {@link TokenWriter} + */ +public enum TokenType { SEPARATOR(false, false), OPERATOR(false, false), From 5e0d36d75f5c36fd8b431fea0e614b7e3c8ae67b Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Tue, 21 Aug 2018 21:03:28 +0200 Subject: [PATCH 03/16] fix license headers --- .../printer/change/CollectionSourceFragment.java | 2 +- .../printer/change/ElementSourceFragment.java | 2 +- .../visitor/printer/change/SourceFragment.java | 2 +- .../printer/change/TokenSourceFragment.java | 2 +- .../visitor/printer/change/TokenType.java | 16 ++++++++++++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java index 4aab9201213..1a74272be50 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2006-2017 INRIA and contributors + * Copyright (C) 2006-2018 INRIA and contributors * Spoon - http://spoon.gforge.inria.fr/ * * This software is governed by the CeCILL-C License under French law and diff --git a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java index 50bcf993d91..c0da0eae360 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2006-2017 INRIA and contributors + * Copyright (C) 2006-2018 INRIA and contributors * Spoon - http://spoon.gforge.inria.fr/ * * This software is governed by the CeCILL-C License under French law and 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 01ef9487a6e..459f813e0f5 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2006-2017 INRIA and contributors + * Copyright (C) 2006-2018 INRIA and contributors * Spoon - http://spoon.gforge.inria.fr/ * * This software is governed by the CeCILL-C License under French law and diff --git a/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java index 1978d926af8..c394ae5f355 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2006-2017 INRIA and contributors + * Copyright (C) 2006-2018 INRIA and contributors * Spoon - http://spoon.gforge.inria.fr/ * * This software is governed by the CeCILL-C License under French law and diff --git a/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java b/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java index d0b9f3b48d7..cb05e317a8d 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java @@ -1,3 +1,19 @@ +/** + * Copyright (C) 2006-2018 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.reflect.visitor.printer.change; import spoon.reflect.visitor.TokenWriter; From 4376d2a9a10bd57f5a94e628e6c9510c608cdf53 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Mon, 27 Aug 2018 20:37:04 +0200 Subject: [PATCH 04/16] fail when getRootSourceFragment is called on package or module --- .../java/spoon/support/reflect/cu/CompilationUnitImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java index f7e35a816f4..68dce1527fd 100644 --- a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java +++ b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java @@ -309,7 +309,12 @@ public SourcePosition getOrCreatePartialSourcePosition() { @Override public ElementSourceFragment getRootSourceFragment() { if (rootFragment == null) { - String originSourceCode = getOriginalSourceCode(); + if (ctModule != null) { + throw new SpoonException("Root source fragment of compilation unit of module is not supported"); + } + if (ctPackage != null && declaredTypes.isEmpty()) { + throw new SpoonException("Root source fragment of compilation unit of package is not supported"); + } rootFragment = new ElementSourceFragment(this, null); for (CtImport imprt : getImports()) { rootFragment.addChild(new ElementSourceFragment(imprt, null /*TODO role for import of CU*/)); From e05fbb3efc21e01c51258391191b2b92041a4376 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Mon, 27 Aug 2018 20:37:26 +0200 Subject: [PATCH 05/16] contract and improvement of testSourcePositionTreeIsCorrectlyOrdered --- src/test/java/spoon/test/main/MainTest.java | 22 +++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/test/java/spoon/test/main/MainTest.java b/src/test/java/spoon/test/main/MainTest.java index bd45143f120..12fe14b4527 100644 --- a/src/test/java/spoon/test/main/MainTest.java +++ b/src/test/java/spoon/test/main/MainTest.java @@ -476,12 +476,19 @@ public void scan(CtRole role, CtElement element) { @Test public void testSourcePositionTreeIsCorrectlyOrdered() { + /* + * contract: the tree of ElementSourceFragments of all spoon types (= sample set of sources) can be built. + * contract: the tree of ElementSourceFragments is correctly organized. It means: + * - source positions of children elements are smaller or equal to their parents + * - source positions of next siblings are after their previous siblings + * - + */ List types = rootPackage.filterChildren(new TypeFilter<>(CtType.class)).filterChildren((CtType t) -> t.isTopLevel()).list(); int totalCount = 0; boolean hasComment = false; for (CtType type : types) { SourcePosition sp = type.getPosition(); - totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getRootSourceFragment()); + totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getRootSourceFragment(), 0, sp.getCompilationUnit().getOriginalSourceCode().length()); hasComment = hasComment || type.getComments().size() > 0; }; assertTrue(totalCount > 1000); @@ -491,19 +498,22 @@ public void testSourcePositionTreeIsCorrectlyOrdered() { /** * Asserts that all siblings and children of sp are well ordered * @param sourceFragment + * @param minOffset TODO + * @param maxOffset TODO * @return number of checked {@link SourcePosition} nodes */ - private int assertSourcePositionTreeIsCorrectlyOrder(ElementSourceFragment sourceFragment) { + private int assertSourcePositionTreeIsCorrectlyOrder(ElementSourceFragment sourceFragment, int minOffset, int maxOffset) { int nr = 0; - int pos = 0; + int pos = minOffset; while (sourceFragment != null) { nr++; - assertTrue(pos <= sourceFragment.getStart()); - assertTrue(sourceFragment.getStart() <= sourceFragment.getEnd()); + assertTrue("min(" + pos + ") <= fragment.start(" + sourceFragment.getStart() + ")", pos <= sourceFragment.getStart()); + assertTrue("fragment.start(" + sourceFragment.getStart() + ") <= fragment.end(" + sourceFragment.getEnd() + ")", sourceFragment.getStart() <= sourceFragment.getEnd()); pos = sourceFragment.getEnd(); - nr += assertSourcePositionTreeIsCorrectlyOrder(sourceFragment.getFirstChild()); + nr += assertSourcePositionTreeIsCorrectlyOrder(sourceFragment.getFirstChild(), sourceFragment.getStart(), sourceFragment.getEnd()); sourceFragment = sourceFragment.getNextSibling(); } + assertTrue("lastFragment.end(" + pos + ") <= max(" + maxOffset + ")", pos <= maxOffset); return nr; } From 918aa77d22e160aa03da268a291c6a52308070b2 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Thu, 6 Sep 2018 18:34:51 +0200 Subject: [PATCH 06/16] apply #2283 --- .../java/spoon/reflect/cu/CompilationUnit.java | 17 ----------------- .../spoon/reflect/cu/SourcePositionHolder.java | 13 ++++++++++++- .../printer/change/ElementSourceFragment.java | 7 ++++++- .../support/reflect/CtExtendedModifier.java | 14 ++++++++++++++ .../support/reflect/cu/CompilationUnitImpl.java | 13 +------------ .../reflect/declaration/CtElementImpl.java | 14 ++++++++++++++ src/test/java/spoon/test/main/MainTest.java | 2 +- .../spoon/test/position/TestSourceFragment.java | 2 +- 8 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/main/java/spoon/reflect/cu/CompilationUnit.java b/src/main/java/spoon/reflect/cu/CompilationUnit.java index fed60343a1e..c92cea03687 100644 --- a/src/main/java/spoon/reflect/cu/CompilationUnit.java +++ b/src/main/java/spoon/reflect/cu/CompilationUnit.java @@ -20,8 +20,6 @@ import spoon.reflect.declaration.CtModule; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; -import spoon.reflect.visitor.printer.change.ElementSourceFragment; -import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtImport; import spoon.support.Experimental; @@ -166,19 +164,4 @@ enum UNIT_TYPE { */ @Experimental void setImports(Set imports); - - /** - * @return root {@link ElementSourceFragment} of the tree of all the source fragments of origin source code of this compilation unit - * Note: this method creates tree of {@link ElementSourceFragment}s ... it can be a lot of instances - * If the tree of {@link ElementSourceFragment}s is needed then it MUST be created - * BEFORE the Spoon model of this {@link CompilationUnit} is changed. The best creation time is immediately after Spoon model is built. - * Otherwise there might be missing some {@link ElementSourceFragment}s when {@link CtElement}s are deleted - */ - ElementSourceFragment getRootSourceFragment(); - - /** - * @param element the {@link CtElement} whose origin source code is needed. - * @return {@link ElementSourceFragment} which mirrors the origin source code of the `element` or null. - */ - ElementSourceFragment getSourceFragment(SourcePositionHolder element); } diff --git a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java index 231a4b56081..de4a4b367bd 100644 --- a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java +++ b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java @@ -16,9 +16,20 @@ */ package spoon.reflect.cu; +import spoon.reflect.visitor.printer.change.ElementSourceFragment; + /** - * This interface represents an element which knows it's position in a source file. + * This interface represents an element which knows its position in a source file. */ public interface SourcePositionHolder { + /** If the element comes from a Java source file (hence has not created during transformation), returns the position in the original source file */ SourcePosition getPosition(); + + /** + * Returns the original source code (maybe different from toString() if a transformation has been applied. + * Or {@link ElementSourceFragment#NO_SOURCE_FRAGMENT} if this element has no original source fragment. + */ + default ElementSourceFragment getOriginalSourceFragment() { + return ElementSourceFragment.NO_SOURCE_FRAGMENT; + } } diff --git a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java index c0da0eae360..6e2a0ea3efe 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java @@ -47,11 +47,16 @@ /** * Represents a part of source code of an {@link CtElement} * It is connected into a tree of {@link ElementSourceFragment}s. - * That tree can be build by {@link CompilationUnit#getRootSourceFragment()} + * That tree can be build by {@link CompilationUnit#getOriginalSourceFragment()} * And the tree of {@link ElementSourceFragment}s related to one element can be returned by {@link CompilationUnit#getSourceFragment(SourcePositionHolder)} */ public class ElementSourceFragment implements SourceFragment { + /** + * represents an {@link ElementSourceFragment}, which doesn't exist + */ + public static final ElementSourceFragment NO_SOURCE_FRAGMENT = new ElementSourceFragment(null, null); + private final SourcePositionHolder element; private final RoleHandler roleHandlerInParent; private ElementSourceFragment nextSibling; diff --git a/src/main/java/spoon/support/reflect/CtExtendedModifier.java b/src/main/java/spoon/support/reflect/CtExtendedModifier.java index db8090a783f..d8bd7beb2b4 100644 --- a/src/main/java/spoon/support/reflect/CtExtendedModifier.java +++ b/src/main/java/spoon/support/reflect/CtExtendedModifier.java @@ -16,9 +16,11 @@ */ package spoon.support.reflect; +import spoon.reflect.cu.CompilationUnit; import spoon.reflect.cu.SourcePosition; import spoon.reflect.cu.SourcePositionHolder; import spoon.reflect.declaration.ModifierKind; +import spoon.reflect.visitor.printer.change.ElementSourceFragment; import java.io.Serializable; @@ -87,4 +89,16 @@ public int hashCode() { result = 31 * result + (kind != null ? kind.hashCode() : 0); return result; } + + @Override + public ElementSourceFragment getOriginalSourceFragment() { + SourcePosition sp = this.getPosition(); + CompilationUnit compilationUnit = sp.getCompilationUnit(); + if (compilationUnit != null) { + ElementSourceFragment rootFragment = (ElementSourceFragment) compilationUnit.getOriginalSourceFragment(); + return rootFragment.getSourceFragmentOf(this, sp.getSourceStart(), sp.getSourceEnd() + 1); + } else { + return ElementSourceFragment.NO_SOURCE_FRAGMENT; + } + } } diff --git a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java index 68dce1527fd..945e21e6763 100644 --- a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java +++ b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java @@ -20,7 +20,6 @@ import spoon.processing.FactoryAccessor; import spoon.reflect.cu.CompilationUnit; import spoon.reflect.cu.SourcePosition; -import spoon.reflect.cu.SourcePositionHolder; import spoon.reflect.declaration.CtImport; import spoon.reflect.declaration.CtModule; import spoon.reflect.declaration.CtPackage; @@ -307,7 +306,7 @@ public SourcePosition getOrCreatePartialSourcePosition() { } @Override - public ElementSourceFragment getRootSourceFragment() { + public ElementSourceFragment getOriginalSourceFragment() { if (rootFragment == null) { if (ctModule != null) { throw new SpoonException("Root source fragment of compilation unit of module is not supported"); @@ -326,16 +325,6 @@ public ElementSourceFragment getRootSourceFragment() { return rootFragment; } - @Override - public ElementSourceFragment getSourceFragment(SourcePositionHolder element) { - ElementSourceFragment rootFragment = getRootSourceFragment(); - SourcePosition sp = element.getPosition(); - if (sp.getCompilationUnit() != this) { - throw new SpoonException("Cannot return SourceFragment of element for different CompilationUnit"); - } - return rootFragment.getSourceFragmentOf(element, sp.getSourceStart(), sp.getSourceEnd() + 1); - } - @Override public int[] getLineSeparatorPositions() { return lineSeparatorPositions; diff --git a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java index dec6a207d7f..1b32c409a57 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java @@ -24,6 +24,7 @@ import spoon.reflect.code.CtComment; import spoon.reflect.code.CtJavaDoc; import spoon.reflect.code.CtJavaDocTag; +import spoon.reflect.cu.CompilationUnit; import spoon.reflect.cu.SourcePosition; import spoon.reflect.declaration.CtAnnotation; import spoon.reflect.declaration.CtElement; @@ -52,6 +53,7 @@ import spoon.reflect.visitor.chain.CtFunction; import spoon.reflect.visitor.chain.CtQuery; import spoon.reflect.visitor.filter.AnnotationFilter; +import spoon.reflect.visitor.printer.change.ElementSourceFragment; import spoon.reflect.visitor.CtIterator; import spoon.support.DefaultCoreFactory; import spoon.support.DerivedProperty; @@ -612,4 +614,16 @@ public Iterator descendantIterator() { public Iterable asIterable() { return this::descendantIterator; } + + @Override + public ElementSourceFragment getOriginalSourceFragment() { + SourcePosition sp = this.getPosition(); + CompilationUnit compilationUnit = sp.getCompilationUnit(); + if (compilationUnit != null) { + ElementSourceFragment rootFragment = (ElementSourceFragment) compilationUnit.getOriginalSourceFragment(); + return rootFragment.getSourceFragmentOf(this, sp.getSourceStart(), sp.getSourceEnd() + 1); + } else { + return ElementSourceFragment.NO_SOURCE_FRAGMENT; + } + } } diff --git a/src/test/java/spoon/test/main/MainTest.java b/src/test/java/spoon/test/main/MainTest.java index 12fe14b4527..75c24c8a543 100644 --- a/src/test/java/spoon/test/main/MainTest.java +++ b/src/test/java/spoon/test/main/MainTest.java @@ -488,7 +488,7 @@ public void testSourcePositionTreeIsCorrectlyOrdered() { boolean hasComment = false; for (CtType type : types) { SourcePosition sp = type.getPosition(); - totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getRootSourceFragment(), 0, sp.getCompilationUnit().getOriginalSourceCode().length()); + totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getOriginalSourceFragment(), 0, sp.getCompilationUnit().getOriginalSourceCode().length()); hasComment = hasComment || type.getComments().size() > 0; }; assertTrue(totalCount > 1000); diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java index f8863053e8e..88ed5f534f5 100644 --- a/src/test/java/spoon/test/position/TestSourceFragment.java +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -180,7 +180,7 @@ public void testSourceFragmentOfIf() throws Exception { } private void checkElementFragments(CtElement ele, Object... expectedFragments) { - ElementSourceFragment fragment = ele.getPosition().getCompilationUnit().getSourceFragment(ele); + ElementSourceFragment fragment = ele.getOriginalSourceFragment(); List children = fragment.getChildrenFragments(); assertEquals(expandGroup(new ArrayList<>(), expectedFragments), childSourceFragmentsToStrings(children)); assertGroupsEqual(expectedFragments, fragment.getGroupedChildrenFragments()); From 1609637ccd0d55f3070f2d68b8f167b2c669d9a1 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Thu, 6 Sep 2018 18:47:40 +0200 Subject: [PATCH 07/16] test source fragments of labeled statement --- src/test/java/spoon/test/position/TestSourceFragment.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java index 88ed5f534f5..b0a5f316b13 100644 --- a/src/test/java/spoon/test/position/TestSourceFragment.java +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -143,7 +143,7 @@ private ElementSourceFragment createFragment(int start, int end) { } @Test - public void testSourceFragmentOfIf() throws Exception { + public void testExactSourceFragments() throws Exception { //contract: SourceFragments of some tricky sources are as expected final Launcher launcher = new Launcher(); launcher.getEnvironment().setNoClasspath(false); @@ -171,8 +171,7 @@ public void testSourceFragmentOfIf() throws Exception { " ", "T", " ", "m3", "(", group("U param", ",", " ", "@Deprecated int p2"), ")", " ", "{\n" + " return null;\n" + " }"); - //TODO uncomment and fix source code of element label - //checkElementFragments(foo.getMethodsByName("m4").get(0).getBody().getStatement(0),""); + checkElementFragments(foo.getMethodsByName("m4").get(0).getBody().getStatement(0),"label",":"," ", "while", "(", "true", ")", ";"); checkElementFragments(foo.getMethodsByName("m5").get(0).getBody().getStatement(0),"f", " ", "=", " ", "7.2", ";"); checkElementFragments(((CtAssignment)foo.getMethodsByName("m5").get(0).getBody().getStatement(0)).getAssignment(),"7.2"); From b8b67dafa942b8dd527c6bce494329631db9e363 Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Thu, 6 Sep 2018 20:19:41 +0200 Subject: [PATCH 08/16] fix javadoc --- .../reflect/visitor/printer/change/ElementSourceFragment.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java index 6e2a0ea3efe..b5239625e0c 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java @@ -47,8 +47,7 @@ /** * Represents a part of source code of an {@link CtElement} * It is connected into a tree of {@link ElementSourceFragment}s. - * That tree can be build by {@link CompilationUnit#getOriginalSourceFragment()} - * And the tree of {@link ElementSourceFragment}s related to one element can be returned by {@link CompilationUnit#getSourceFragment(SourcePositionHolder)} + * Use {@link SourcePositionHolder#getOriginalSourceFragment()} to get it. */ public class ElementSourceFragment implements SourceFragment { From 40bfa1bffaa303b608ca9e511858f2a30466baaf Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Thu, 6 Sep 2018 22:27:40 +0200 Subject: [PATCH 09/16] up --- chore/check-links-in-doc.py | 2 +- .../spoon/reflect/cu/CompilationUnit.java | 1 + .../reflect/cu/SourcePositionHolder.java | 2 +- .../printer/{change => }/SourceFragment.java | 2 +- .../change/CollectionSourceFragment.java | 86 -- .../printer/change/ElementSourceFragment.java | 878 ------------------ .../printer/change/TokenSourceFragment.java | 49 - .../visitor/printer/change/TokenType.java | 52 -- .../support/reflect/CtExtendedModifier.java | 2 +- .../reflect/cu/CompilationUnitImpl.java | 2 +- .../reflect/declaration/CtElementImpl.java | 2 +- src/test/java/spoon/test/main/MainTest.java | 3 +- .../test/position/TestSourceFragment.java | 6 +- 13 files changed, 11 insertions(+), 1076 deletions(-) rename src/main/java/spoon/reflect/visitor/printer/{change => }/SourceFragment.java (95%) delete mode 100644 src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java delete mode 100644 src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java delete mode 100644 src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java delete mode 100644 src/main/java/spoon/reflect/visitor/printer/change/TokenType.java diff --git a/chore/check-links-in-doc.py b/chore/check-links-in-doc.py index 29c6cc855db..b257f5b2418 100644 --- a/chore/check-links-in-doc.py +++ b/chore/check-links-in-doc.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.7 # checks the links of the Spoon documentation # a problem is reported as an exception, hence as a Unic return code != -1, hence as a build failure diff --git a/src/main/java/spoon/reflect/cu/CompilationUnit.java b/src/main/java/spoon/reflect/cu/CompilationUnit.java index c92cea03687..cfd1d14ba35 100644 --- a/src/main/java/spoon/reflect/cu/CompilationUnit.java +++ b/src/main/java/spoon/reflect/cu/CompilationUnit.java @@ -164,4 +164,5 @@ enum UNIT_TYPE { */ @Experimental void setImports(Set imports); + } diff --git a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java index de4a4b367bd..92f76176f33 100644 --- a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java +++ b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java @@ -16,7 +16,7 @@ */ package spoon.reflect.cu; -import spoon.reflect.visitor.printer.change.ElementSourceFragment; +import spoon.reflect.visitor.printer.internal.ElementSourceFragment; /** * This interface represents an element which knows its position in a source file. diff --git a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java similarity index 95% rename from src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java rename to src/main/java/spoon/reflect/visitor/printer/SourceFragment.java index 459f813e0f5..c231dfad8a9 100644 --- a/src/main/java/spoon/reflect/visitor/printer/change/SourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java @@ -14,7 +14,7 @@ * 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.reflect.visitor.printer.change; +package spoon.reflect.visitor.printer; /** */ diff --git a/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java deleted file mode 100644 index 1a74272be50..00000000000 --- a/src/main/java/spoon/reflect/visitor/printer/change/CollectionSourceFragment.java +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (C) 2006-2018 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.reflect.visitor.printer.change; - -import java.util.List; - -import spoon.reflect.meta.ContainerKind; -import spoon.reflect.path.CtRole; - -/** - * {@link SourceFragment} of List or Set of {@link ElementSourceFragment}s which belongs to collection role. - * For example list of Type members or list of parameters, etc. - * Or set of modifiers and annotations - */ -public class CollectionSourceFragment implements SourceFragment { - - private final List items; - - public CollectionSourceFragment(List items) { - super(); - this.items = items; - } - - @Override - public String getSourceCode() { - StringBuilder sb = new StringBuilder(); - for (SourceFragment childSourceFragment : items) { - sb.append(childSourceFragment.getSourceCode()); - } - return sb.toString(); - } - - /** - * @return child source fragments of this collection - */ - public List getItems() { - return items; - } - - @Override - public String toString() { - return items.toString(); - } - - /** - * @return true if collection contains only children of one role handler with container kind LIST - */ - public boolean isOrdered() { - CtRole role = null; - ContainerKind kind = null; - for (SourceFragment childSourceFragment : items) { - if (childSourceFragment instanceof ElementSourceFragment) { - ElementSourceFragment esf = (ElementSourceFragment) childSourceFragment; - if (role == null) { - role = esf.getRoleInParent(); - kind = esf.getContainerKindInParent(); - if (kind != ContainerKind.LIST) { - return false; - } - } else { - if (role != esf.getRoleInParent()) { - //the collection contains elements of different roles. It cannot be ordered - return false; - } - //else there is another element of the same role - ok - } - } - } - //there are only elements of one role of container kind LIST - return true; - } -} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java deleted file mode 100644 index b5239625e0c..00000000000 --- a/src/main/java/spoon/reflect/visitor/printer/change/ElementSourceFragment.java +++ /dev/null @@ -1,878 +0,0 @@ -/** - * Copyright (C) 2006-2018 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.reflect.visitor.printer.change; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Deque; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import spoon.SpoonException; -import spoon.reflect.code.CtComment; -import spoon.reflect.code.CtLiteral; -import spoon.reflect.cu.CompilationUnit; -import spoon.reflect.cu.SourcePosition; -import spoon.reflect.cu.SourcePositionHolder; -import spoon.reflect.declaration.CtElement; -import spoon.reflect.declaration.CtModifiable; -import spoon.reflect.meta.ContainerKind; -import spoon.reflect.meta.RoleHandler; -import spoon.reflect.meta.impl.RoleHandlerHelper; -import spoon.reflect.path.CtRole; -import spoon.reflect.visitor.CtScanner; -import spoon.support.reflect.CtExtendedModifier; -import spoon.support.reflect.cu.position.SourcePositionImpl; - -/** - * Represents a part of source code of an {@link CtElement} - * It is connected into a tree of {@link ElementSourceFragment}s. - * Use {@link SourcePositionHolder#getOriginalSourceFragment()} to get it. - */ -public class ElementSourceFragment implements SourceFragment { - - /** - * represents an {@link ElementSourceFragment}, which doesn't exist - */ - public static final ElementSourceFragment NO_SOURCE_FRAGMENT = new ElementSourceFragment(null, null); - - private final SourcePositionHolder element; - private final RoleHandler roleHandlerInParent; - private ElementSourceFragment nextSibling; - private ElementSourceFragment firstChild; - - /** - * Creates a source fragment of {@link SourcePositionHolder} - * - * @param element target {@link SourcePositionHolder} - * @param roleHandlerInParent The {@link RoleHandler}, which defines role of target `element` in it's parent - */ - public ElementSourceFragment(SourcePositionHolder element, RoleHandler roleHandlerInParent) { - this.element = element; - this.roleHandlerInParent = roleHandlerInParent; - } - - /** - * @return offset of first character which belongs to this fragment - */ - public int getStart() { - if (firstChild != null) { - return Math.min(getSourcePosition().getSourceStart(), firstChild.getStart()); - } - return getSourcePosition().getSourceStart(); - } - - /** - * @return offset of character after this fragment - */ - public int getEnd() { - if (firstChild != null) { - return Math.max(getSourcePosition().getSourceEnd() + 1, firstChild.getLastSibling().getEnd()); - } - return getSourcePosition().getSourceEnd() + 1; - } - - /** - * @return {@link SourcePosition} of this fragment - */ - public SourcePosition getSourcePosition() { - return element.getPosition(); - } - - @Override - public String toString() { - return "|" + getStart() + ", " + getEnd() + "|" + getSourceCode() + "|"; - } - - /** - * @return origin source code of this fragment - */ - @Override - public String getSourceCode() { - return getSourceCode(getStart(), getEnd()); - } - - /** - * @param start start offset relative to compilation unit - * @param end end offset (after last character) relative to compilation unit - * @return source code of this Fragment between start/end offsets - */ - public String getSourceCode(int start, int end) { - String src = getOriginalSourceCode(); - if (src != null) { - return src.substring(start, end); - } - return null; - } - - /** - * @return true if position points to same compilation unit (source file) as this SourceFragment - */ - private boolean isFromSameSource(SourcePosition position) { - return getSourcePosition().getCompilationUnit().equals(position.getCompilationUnit()); - } - - /** - * Builds tree of {@link SourcePosition}s of `element` and all it's children - * @param element the root element of the tree - */ - public void addTreeOfSourceFragmentsOfElement(CtElement element) { - SourcePosition sp = element.getPosition(); - Deque parents = new ArrayDeque<>(); - parents.push(this); - //scan all children of `element` and build tree of SourceFragments - new CtScanner() { - CtRole scannedRole; - public void scan(CtRole role, CtElement element) { - scannedRole = role; - super.scan(role, element); - } - @Override - protected void enter(CtElement e) { - ElementSourceFragment newFragment = addChild(parents.peek(), scannedRole, e); - if (newFragment != null) { - parents.push(newFragment); - if (e instanceof CtModifiable) { - CtModifiable modifiable = (CtModifiable) e; - Set modifiers = modifiable.getExtendedModifiers(); - for (CtExtendedModifier ctExtendedModifier : modifiers) { - addChild(newFragment, CtRole.MODIFIER, ctExtendedModifier); - } - } - } - } - @Override - protected void exit(CtElement e) { - ElementSourceFragment topFragment = parents.peek(); - if (topFragment != null && topFragment.getElement() == e) { - parents.pop(); - } - } - }.scan(element.getRoleInParent(), element); - } - /** - * @param parentFragment the parent {@link ElementSourceFragment}, which will receive {@link ElementSourceFragment} made for `otherElement` - * @param roleInParent the {@link CtRole} of `otherElement` in scope of element of `parentFragment` - * @param otherElement {@link SourcePositionHolder} whose {@link ElementSourceFragment} has to be added to `parentFragment` - * @return new {@link ElementSourceFragment} created for `otherElement` or null if `otherElement` has no source position or doesn't belong to the same compilation unit - */ - private ElementSourceFragment addChild(ElementSourceFragment parentFragment, CtRole roleInParent, SourcePositionHolder otherElement) { - SourcePosition otherSourcePosition = otherElement.getPosition(); - if (otherSourcePosition instanceof SourcePositionImpl && otherSourcePosition.getCompilationUnit() != null) { - SourcePositionImpl childSPI = (SourcePositionImpl) otherSourcePosition; - if (parentFragment.isFromSameSource(otherSourcePosition)) { - ElementSourceFragment otherFragment = new ElementSourceFragment(otherElement, parentFragment.getRoleHandler(roleInParent, otherElement)); - //parent and child are from the same file. So we can connect their positions into one tree - CMP cmp = parentFragment.compare(otherFragment); - if (cmp == CMP.OTHER_IS_CHILD) { - //child belongs under parent - OK - parentFragment.addChild(otherFragment); - return otherFragment; - } else { - if (cmp == CMP.OTHER_IS_AFTER || cmp == CMP.OTHER_IS_BEFORE) { - if (otherElement instanceof CtComment) { - /* - * comments of elements are sometime not included in source position of element. - * because comments are ignored tokens for java compiler, which computes start/end of elements - * Example: - * - * //a comment - * aStatement(); - * - */ - if (otherFragment.getStart() == 0) { - //it is CompilationUnit comment, which is before package and imports, so it doesn't belong to class - //No problem. Simply add comment at correct position into SourceFragment tree, starting from ROOT - addChild(otherFragment); - return otherFragment; - } - //add this child into parent's source fragment and extend that parent source fragment - parentFragment.addChild(otherFragment); - return otherFragment; - } - } - //the source position of child element is not included in source position of parent element - //I (Pavel) am not sure how to handle it, so let's wait until it happens... -// 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 ... - throw new SpoonException("The SourcePosition of elements are not consistent\nparentFragment: " + parentFragment + "\notherFragment: " + otherFragment); - } - } else { - throw new SpoonException("SourcePosition from unexpected compilation unit: " + otherSourcePosition + " expected is: " + parentFragment.getSourcePosition()); - } - } - //do not connect that undefined source position - return null; - } - - private RoleHandler getRoleHandler(CtRole roleInParent, SourcePositionHolder otherElement) { - SourcePositionHolder parent = element; - if (parent instanceof CompilationUnit) { - parent = null; - } - if (parent == null) { - if (otherElement instanceof CtElement) { - parent = ((CtElement) otherElement).getParent(); - } - } - if (parent instanceof CtElement) { - CtElement ele = (CtElement) parent; - return RoleHandlerHelper.getRoleHandler(ele.getClass(), roleInParent); - } - return null; - } - - /** - * adds `other` {@link ElementSourceFragment} into tree of {@link ElementSourceFragment}s represented by this root element - * - * @param other to be added {@link ElementSourceFragment} - * @return new root of the tree of the {@link ElementSourceFragment}s. It can be be this or `other` - */ - public ElementSourceFragment add(ElementSourceFragment 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, merge this and all siblings of `this` as children and siblings of `other` - other.merge(this); - return other; - } - throw new SpoonException("Unexpected compare result: " + cmp); - } - - private void merge(ElementSourceFragment tobeMerged) { - while (tobeMerged != null) { - ElementSourceFragment nextTobeMerged = tobeMerged.getNextSibling(); - //disconnect tobeMerged from nextSiblings before we add it. So it is added individually and not with wrong siblings too - tobeMerged.nextSibling = null; - add(tobeMerged); - tobeMerged = nextTobeMerged; - } - } - - /** - * adds `fragment` as child fragment of this fragment. If child is located before or after this fragment, - * then start/end of this fragment is moved - * @param fragment to be add - */ - public void addChild(ElementSourceFragment fragment) { - if (firstChild == null) { - firstChild = fragment; - } else { - firstChild = firstChild.add(fragment); - } - } - - private void addNextSibling(ElementSourceFragment sibling) { - if (nextSibling == null) { - nextSibling = sibling; - } else { - nextSibling = nextSibling.add(sibling); - } - } - - private ElementSourceFragment getLastSibling() { - ElementSourceFragment lastSibling = this; - while (lastSibling.nextSibling != null) { - lastSibling = lastSibling.nextSibling; - } - return lastSibling; - } - - 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(ElementSourceFragment 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() + "\"]"); - } - - /** - * @return {@link ElementSourceFragment} which belongs to the same parent and is next in the sources - */ - public ElementSourceFragment getNextSibling() { - return nextSibling; - } - - /** - * @return {@link ElementSourceFragment}, which is first child of this fragment - */ - public ElementSourceFragment getFirstChild() { - return firstChild; - } - - /** - * Searches the tree of fragments for the {@link ElementSourceFragment} with expected `element`, - * which contains `start` and `end` source interval. - * It searches in siblings and children of this {@link ElementSourceFragment} recursively. - * @param element the {@link SourcePositionHolder} of fragment it is looking for or null for any element - * @param start the start offset of searched fragment - * @param end the offset of next character after the end of searched fragment - * - * @return {@link ElementSourceFragment} which represents the root of the CtElement whose sources are in interval [start, end] - */ - public ElementSourceFragment getSourceFragmentOf(SourcePositionHolder element, int start, int end) { - int myEnd = getEnd(); - if (myEnd <= start) { - //search in next sibling - if (nextSibling == null) { - return null; - } - return getRootFragmentOfElement(nextSibling.getSourceFragmentOf(element, start, end)); - } - int myStart = getStart(); - if (myStart <= start) { - if (myEnd >= end) { - if (myStart == start && myEnd == end) { - //we have found exact match - if (element != null && getElement() != element) { - if (firstChild == null) { - throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); - } - return firstChild.getSourceFragmentOf(element, start, end); - } - return this; - } - //it is the child - if (firstChild == null) { - if (element != null && getElement() != element) { - throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); - } - return this; - } - ElementSourceFragment child = getRootFragmentOfElement(firstChild.getSourceFragmentOf(element, start, end)); - if (child != null) { - //all children are smaller then this element - return child; - } - //so this fragment is last one which wraps whole element - if (element != null && getElement() != element) { - throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); - } - return this; - } - //start - end overlaps over multiple fragments - throw new SpoonException("Invalid start/end interval. It overlaps multiple fragments."); - } - return null; - } - - private ElementSourceFragment getRootFragmentOfElement(ElementSourceFragment 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. - * May be null - */ - public SourcePositionHolder getElement() { - return element; - } - - /** - * Note: the List of children is flat. The child fragments of collections (parameters, type members, ...) are next to each other. - * @return list of child fragments of this {@link ElementSourceFragment}. - */ - public List getChildrenFragments() { - if (element instanceof CtLiteral) { - return Collections.singletonList(new TokenSourceFragment(getSourceCode(), TokenType.LITERAL)); - } - List children = new ArrayList<>(); - int off = getStart(); - ElementSourceFragment child = getFirstChild(); - while (child != null) { - forEachConstantFragment(off, child.getStart(), cf -> children.add(cf)); - children.add(child); - off = child.getEnd(); - child = child.getNextSibling(); - } - forEachConstantFragment(off, getEnd(), cf -> children.add(cf)); - return children; - } - - /** - * Detects all child fragments of this {@link ElementSourceFragment}. - * Note: the List of children contains one {@link CollectionSourceFragment} for each collection of fragments (parameters, type members, ...). - * Note: the {@link CollectionSourceFragment} may contain a mix of fragments of different roles, when they overlap. - * For example this code contains mix of annotations and modifiers - * public @Deprecated static @Ignored void method() - * @return list of child fragments of this {@link ElementSourceFragment} where fragments, - * which belongs to the same collection are grouped into {@link CollectionSourceFragment} - */ - public List getGroupedChildrenFragments() { - List flatChildren = getChildrenFragments(); - List result = new ArrayList<>(); - int i = 0; - while (i < flatChildren.size()) { - SourceFragment child = flatChildren.get(i); - if (child instanceof TokenSourceFragment) { - result.add(child); - i++; - continue; - } else if (child instanceof ElementSourceFragment) { - ElementSourceFragment esf = (ElementSourceFragment) child; - ContainerKind kind = esf.getContainerKindInParent(); - if (kind == ContainerKind.SINGLE) { - //it is root element or there is always only one child instance in parent - result.add(child); - i++; - continue; - } - //there can be 0, 1 or more items of children of the same role - //search for another element of the same role - Set foundRoles = new HashSet<>(); - foundRoles.add(checkNotNull(esf.getRoleInParent())); - List childrenInSameCollection = new ArrayList<>(); - //but first include prefix whitespace - SourceFragment spaceChild = removeSuffixSpace(result); - if (spaceChild != null) { - childrenInSameCollection.add(spaceChild); - } - childrenInSameCollection.add(esf); - int lastOfSameRole = findIndexOfLastChildTokenOfRoleHandler(flatChildren, i, esf.getRoleInParent()); - //search for other roles in that interval - i++; - while (i <= lastOfSameRole) { - child = flatChildren.get(i); - childrenInSameCollection.add(child); - CtRole role = null; - if (child instanceof ElementSourceFragment) { - ElementSourceFragment esf2 = (ElementSourceFragment) child; - role = esf2.getRoleInParent(); - } - if (role != null && role != CtRole.COMMENT && foundRoles.add(role)) { - //there is another role in same block, search for last one - lastOfSameRole = Math.max(lastOfSameRole, findIndexOfLastChildTokenOfRoleHandler(flatChildren, i + 1, role)); - } - i++; - } - //add suffix space - if (i < flatChildren.size()) { - SourceFragment nextChild = flatChildren.get(i); - if (isSpaceFragment(nextChild)) { - childrenInSameCollection.add(nextChild); - i++; - } - } - result.add(new CollectionSourceFragment(childrenInSameCollection)); - } else { - throw new SpoonException("Unexpected SourceFragment of type " + child.getClass()); - } - } - return result; - } - - private SourceFragment removeSuffixSpace(List list) { - if (list.size() > 0) { - SourceFragment lastChild = list.get(list.size() - 1); - if (isSpaceFragment(lastChild)) { - list.remove(list.size() - 1); - return lastChild; - } - } - return null; - } - - private T checkNotNull(T o) { - if (o == null) { - throw new SpoonException("Unexpected null value"); - } - return o; - } - - private static int findIndexOfLastChildTokenOfRoleHandler(List childFragments, int start, CtRole role) { - return findIndexOfPreviousFragment(childFragments, start, - filter(ElementSourceFragment.class, fragment -> fragment.getRoleInParent() == role)); - } - - private enum CharType { - SPACE, - NON_SPACE; - - static CharType fromChar(char c) { - return Character.isWhitespace(c) ? SPACE : NON_SPACE; - } - } - - private static final Set separators = new HashSet<>(Arrays.asList("->", "::", "...")); - static { - "(){}[];,.:@=<>?&|".chars().forEach(c -> separators.add(new String(Character.toChars(c)))); - } - private static final Set operators = new HashSet<>(Arrays.asList( - "=", - ">", - "<", - "!", - "~", - "?", - ":", - "==", - "<=", - ">=", - "!=", - "&&", - "||", - "++", - "--", - "+", - "-", - "*", - "/", - "&", - "|", - "^", - "%", - "<<", ">>", ">>>", - - "+=", - "-=", - "*=", - "/=", - "&=", - "|=", - "^=", - "%=", - "<<=", - ">>=", - ">>>="/*, - it is handled as keyword here - "instanceof" - */ - )); - - private static final String[] javaKeywordsJoined = new String[] { - "abstract continue for new switch", - "assert default goto package synchronized", - "boolean do if private this", - "break double implements protected throw", - "byte else import public throws", - "case enum instanceof return transient", - "catch extends int short try", - "char final interface static void", - "class finally long strictfp volatile", - "const float native super while"}; - - private static final Set javaKeywords = new HashSet<>(); - static { - for (String str : javaKeywordsJoined) { - StringTokenizer st = new StringTokenizer(str, " "); - while (st.hasMoreTokens()) { - javaKeywords.add(st.nextToken()); - } - } - } - - private static final List matchers = new ArrayList<>(); - static { - separators.forEach(s -> matchers.add(new StringMatcher(s, TokenType.SEPARATOR))); - operators.forEach(s -> matchers.add(new StringMatcher(s, TokenType.OPERATOR))); - } - - /** - * Calls `consumer` once for each constant {@link SourceFragment} found in source code between `start` and `end` - */ - private void forEachConstantFragment(int start, int end, Consumer consumer) { - if (start == end) { - return; - } - if (start > end) { - throw new SpoonException("Inconsistent start/end. Start=" + start + " is greater then End=" + end); - } - String sourceCode = getOriginalSourceCode(); - StringBuffer buff = new StringBuffer(); - CharType lastType = null; - int off = start; - while (off < end) { - char c = sourceCode.charAt(off); - CharType type = CharType.fromChar(c); - if (type != lastType) { - if (lastType != null) { - onCharSequence(lastType, buff, consumer); - buff.setLength(0); - } - lastType = type; - } - buff.append(c); - off++; - } - onCharSequence(lastType, buff, consumer); - } - - private void onCharSequence(CharType type, StringBuffer buff, Consumer consumer) { - if (type == CharType.SPACE) { - consumer.accept(new TokenSourceFragment(buff.toString(), TokenType.SPACE)); - return; - } - char[] str = new char[buff.length()]; - buff.getChars(0, buff.length(), str, 0); - int off = 0; - while (off < str.length) { - //detect java identifier or keyword - int lenOfIdentifier = detectJavaIdentifier(str, off); - if (lenOfIdentifier > 0) { - String identifier = new String(str, off, lenOfIdentifier); - if (javaKeywords.contains(identifier)) { - //it is a java keyword - consumer.accept(new TokenSourceFragment(identifier, TokenType.KEYWORD)); - } else { - //it is a java identifier - consumer.accept(new TokenSourceFragment(identifier, TokenType.IDENTIFIER)); - } - off += lenOfIdentifier; - continue; - } - //detect longest match in matchers - StringMatcher longestMatcher = null; - for (StringMatcher strMatcher : matchers) { - if (strMatcher.isMatch(str, off)) { - longestMatcher = strMatcher.getLonger(longestMatcher); - } - } - if (longestMatcher == null) { - throw new SpoonException("Unexpected source text: " + buff.toString()); - } - consumer.accept(new TokenSourceFragment(longestMatcher.toString(), longestMatcher.getType())); - off += longestMatcher.getLength(); - } - } - - /** - * @return number of characters in buff starting from start which are java identifier - */ - private int detectJavaIdentifier(char[] buff, int start) { - int len = buff.length; - int o = start; - if (start <= len) { - char c = buff[o]; - if (Character.isJavaIdentifierStart(c)) { - o++; - while (o < len) { - c = buff[o]; - if (Character.isJavaIdentifierPart(c) == false) { - break; - } - o++; - } - } - } - return o - start; - } - - private String getOriginalSourceCode() { - CompilationUnit cu = getSourcePosition().getCompilationUnit(); - if (cu != null) { - return cu.getOriginalSourceCode(); - } - return null; - } - - private static final class StringMatcher { - private final TokenType type; - private final char[] chars; - - private StringMatcher(final String str, TokenType type) { - super(); - this.type = type; - chars = str.toCharArray(); - } - - public boolean isMatch(final char[] buffer, int pos) { - final int len = chars.length; - if (pos + len > buffer.length) { - return false; - } - for (int i = 0; i < chars.length; i++, pos++) { - if (chars[i] != buffer[pos]) { - return false; - } - } - return true; - } - - @Override - public String toString() { - return new String(chars); - } - - public int getLength() { - return chars.length; - } - - public StringMatcher getLonger(StringMatcher m) { - if (m != null && m.getLength() > getLength()) { - return m; - } - return this; - } - - public TokenType getType() { - return type; - } - } - - /** - * @return role of the element of this fragment in scope of it's parent - */ - public CtRole getRoleInParent() { - return roleHandlerInParent != null ? roleHandlerInParent.getRole() : null; - } - - /** - * @return the {@link ContainerKind} of the attribute which holds the element of this fragment in it's parent - */ - public ContainerKind getContainerKindInParent() { - if (roleHandlerInParent != null) { - if (roleHandlerInParent.getRole() != CtRole.COMMENT) { - return roleHandlerInParent.getContainerKind(); - } - } - return ContainerKind.SINGLE; - } - /** - * looks for next fragment whose {@link Predicate} `test` returns true - * @param start - the index of first to be checked fragment - * @return index of found fragment, or -1 if not found - */ - static int findIndexOfNextFragment(List fragments, int start, Predicate test) { - while (start < fragments.size()) { - SourceFragment fragment = fragments.get(start); - if (test.test(fragment)) { - return start; - } - start++; - } - return -1; - } - - /** - * @param start the index of element with lower index which is checked and may be returned - * @param test a {@link Predicate}, which is evaluated for each item of `fragments` starting from last one and ending with item in index `start` - * @return index of found fragment, or -1 if not found - */ - static int findIndexOfPreviousFragment(List fragments, int start, Predicate test) { - int i = fragments.size() - 1; - while (i >= start) { - if (test.test(fragments.get(i))) { - return i; - } - i--; - } - return -1; - } - - /** - * @param predicate the {@link Predicate}, which has to be checkd for each item of {@link CollectionSourceFragment} - * @return {@link Predicate} which calls `predicate` for each item of {@link CollectionSourceFragment} - * Returned {@link Predicate} returns true only if `predicate` returns true on at least one item - */ - static Predicate checkCollectionItems(Predicate predicate) { - return (SourceFragment fragment) -> { - if (fragment instanceof CollectionSourceFragment) { - CollectionSourceFragment collectionFragment = (CollectionSourceFragment) fragment; - for (SourceFragment itemFragment : collectionFragment.getItems()) { - if (predicate.test(itemFragment)) { - return true; - } - } - return false; - } else { - return predicate.test(fragment); - } - }; - } - - /** - * @param predicate to be called {@link Predicate} - * @return {@link Predicate} which calls `predicate` only for {@link SourceFragment}s of of type `clazz` and returns false for others - */ - static Predicate filter(Class clazz, Predicate predicate) { - return fragment -> { - if (clazz.isInstance(fragment)) { - return predicate.test((T) fragment); - } - return false; - }; - } - - /** - * @return true if {@link SourceFragment} represents a white space - */ - static boolean isSpaceFragment(SourceFragment fragment) { - return fragment instanceof TokenSourceFragment && ((TokenSourceFragment) fragment).getType() == TokenType.SPACE; - } -} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java deleted file mode 100644 index c394ae5f355..00000000000 --- a/src/main/java/spoon/reflect/visitor/printer/change/TokenSourceFragment.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (C) 2006-2018 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.reflect.visitor.printer.change; - -/** - * a {@link SourceFragment} of some primitive String token. - */ -public class TokenSourceFragment implements SourceFragment { - - private final String source; - private final TokenType type; - - public TokenSourceFragment(String source, TokenType type) { - super(); - this.source = source; - this.type = type; - } - - @Override - public String getSourceCode() { - return source; - } - - /** - * @return type of token of this fragment - */ - public TokenType getType() { - return type; - } - - @Override - public String toString() { - return "|" + getSourceCode() + "|"; - } -} diff --git a/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java b/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java deleted file mode 100644 index cb05e317a8d..00000000000 --- a/src/main/java/spoon/reflect/visitor/printer/change/TokenType.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (C) 2006-2018 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.reflect.visitor.printer.change; - -import spoon.reflect.visitor.TokenWriter; - -/** - * Type of {@link TokenSourceFragment} token. - * Note: These types mirrors the methods of {@link TokenWriter} - */ -public enum TokenType { - - SEPARATOR(false, false), - OPERATOR(false, false), - LITERAL(false, false), - KEYWORD(false, false), - IDENTIFIER(false, false), - CODE_SNIPPET(false, false), - COMMENT(false, false), - NEW_LINE(true, false), - INC_TAB(true, true), - DEC_TAB(true, true), - SPACE(true, false); - - private final boolean whiteSpace; - private final boolean tab; - - TokenType(boolean whiteSpace, boolean tab) { - this.whiteSpace = whiteSpace; - this.tab = tab; - } - boolean isWhiteSpace() { - return whiteSpace; - } - public boolean isTab() { - return tab; - } -} diff --git a/src/main/java/spoon/support/reflect/CtExtendedModifier.java b/src/main/java/spoon/support/reflect/CtExtendedModifier.java index d8bd7beb2b4..6da4a2f7cef 100644 --- a/src/main/java/spoon/support/reflect/CtExtendedModifier.java +++ b/src/main/java/spoon/support/reflect/CtExtendedModifier.java @@ -20,7 +20,7 @@ import spoon.reflect.cu.SourcePosition; import spoon.reflect.cu.SourcePositionHolder; import spoon.reflect.declaration.ModifierKind; -import spoon.reflect.visitor.printer.change.ElementSourceFragment; +import spoon.reflect.visitor.printer.internal.ElementSourceFragment; import java.io.Serializable; diff --git a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java index 945e21e6763..aecebf8bf44 100644 --- a/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java +++ b/src/main/java/spoon/support/reflect/cu/CompilationUnitImpl.java @@ -27,7 +27,7 @@ import spoon.reflect.factory.Factory; import spoon.reflect.visitor.DefaultJavaPrettyPrinter; import spoon.reflect.visitor.filter.TypeFilter; -import spoon.reflect.visitor.printer.change.ElementSourceFragment; +import spoon.reflect.visitor.printer.internal.ElementSourceFragment; import spoon.support.reflect.cu.position.PartialSourcePositionImpl; import java.io.File; diff --git a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java index 1b32c409a57..0ec5ed1e78d 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java @@ -53,7 +53,7 @@ import spoon.reflect.visitor.chain.CtFunction; import spoon.reflect.visitor.chain.CtQuery; import spoon.reflect.visitor.filter.AnnotationFilter; -import spoon.reflect.visitor.printer.change.ElementSourceFragment; +import spoon.reflect.visitor.printer.internal.ElementSourceFragment; import spoon.reflect.visitor.CtIterator; import spoon.support.DefaultCoreFactory; import spoon.support.DerivedProperty; diff --git a/src/test/java/spoon/test/main/MainTest.java b/src/test/java/spoon/test/main/MainTest.java index 75c24c8a543..9201e3f5aa4 100644 --- a/src/test/java/spoon/test/main/MainTest.java +++ b/src/test/java/spoon/test/main/MainTest.java @@ -31,7 +31,6 @@ import spoon.reflect.path.CtPathException; import spoon.reflect.path.CtPathStringBuilder; import spoon.reflect.path.CtRole; -import spoon.reflect.reference.CtArrayTypeReference; import spoon.reflect.reference.CtExecutableReference; import spoon.reflect.reference.CtFieldReference; import spoon.reflect.reference.CtTypeParameterReference; @@ -40,7 +39,7 @@ import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.PrinterHelper; import spoon.reflect.visitor.filter.TypeFilter; -import spoon.reflect.visitor.printer.change.ElementSourceFragment; +import spoon.reflect.visitor.printer.internal.ElementSourceFragment; import spoon.support.reflect.CtExtendedModifier; import spoon.test.parent.ParentTest; diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java index b0a5f316b13..2b9da167928 100644 --- a/src/test/java/spoon/test/position/TestSourceFragment.java +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -20,9 +20,9 @@ import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtType; import spoon.reflect.factory.Factory; -import spoon.reflect.visitor.printer.change.SourceFragment; -import spoon.reflect.visitor.printer.change.CollectionSourceFragment; -import spoon.reflect.visitor.printer.change.ElementSourceFragment; +import spoon.reflect.visitor.printer.SourceFragment; +import spoon.reflect.visitor.printer.internal.CollectionSourceFragment; +import spoon.reflect.visitor.printer.internal.ElementSourceFragment; import spoon.support.reflect.cu.CompilationUnitImpl; import spoon.support.reflect.cu.position.SourcePositionImpl; import spoon.test.position.testclasses.FooSourceFragments; From 2728369a2b23924e2f5a5edd239e21868424f89a Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Thu, 6 Sep 2018 22:31:09 +0200 Subject: [PATCH 10/16] up --- src/main/java/spoon/reflect/cu/SourcePositionHolder.java | 3 ++- src/test/java/spoon/test/main/MainTest.java | 2 +- src/test/java/spoon/test/position/TestSourceFragment.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java index 92f76176f33..6567b74f67a 100644 --- a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java +++ b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java @@ -16,6 +16,7 @@ */ package spoon.reflect.cu; +import spoon.reflect.visitor.printer.SourceFragment; import spoon.reflect.visitor.printer.internal.ElementSourceFragment; /** @@ -29,7 +30,7 @@ public interface SourcePositionHolder { * Returns the original source code (maybe different from toString() if a transformation has been applied. * Or {@link ElementSourceFragment#NO_SOURCE_FRAGMENT} if this element has no original source fragment. */ - default ElementSourceFragment getOriginalSourceFragment() { + default SourceFragment getOriginalSourceFragment() { return ElementSourceFragment.NO_SOURCE_FRAGMENT; } } diff --git a/src/test/java/spoon/test/main/MainTest.java b/src/test/java/spoon/test/main/MainTest.java index 9201e3f5aa4..1927f9999ec 100644 --- a/src/test/java/spoon/test/main/MainTest.java +++ b/src/test/java/spoon/test/main/MainTest.java @@ -487,7 +487,7 @@ public void testSourcePositionTreeIsCorrectlyOrdered() { boolean hasComment = false; for (CtType type : types) { SourcePosition sp = type.getPosition(); - totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getOriginalSourceFragment(), 0, sp.getCompilationUnit().getOriginalSourceCode().length()); + totalCount += assertSourcePositionTreeIsCorrectlyOrder((ElementSourceFragment) sp.getCompilationUnit().getOriginalSourceFragment(), 0, sp.getCompilationUnit().getOriginalSourceCode().length()); hasComment = hasComment || type.getComments().size() > 0; }; assertTrue(totalCount > 1000); diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java index 2b9da167928..b53ac4251fd 100644 --- a/src/test/java/spoon/test/position/TestSourceFragment.java +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -179,7 +179,7 @@ public void testExactSourceFragments() throws Exception { } private void checkElementFragments(CtElement ele, Object... expectedFragments) { - ElementSourceFragment fragment = ele.getOriginalSourceFragment(); + ElementSourceFragment fragment = (ElementSourceFragment) ele.getOriginalSourceFragment(); List children = fragment.getChildrenFragments(); assertEquals(expandGroup(new ArrayList<>(), expectedFragments), childSourceFragmentsToStrings(children)); assertGroupsEqual(expectedFragments, fragment.getGroupedChildrenFragments()); From 3650f560c4d1bdac18a34921f209a738fc029f74 Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Thu, 6 Sep 2018 22:37:55 +0200 Subject: [PATCH 11/16] up --- .../visitor/printer/SourceFragment.java | 1 + .../internal/CollectionSourceFragment.java | 87 ++ .../internal/ElementSourceFragment.java | 879 ++++++++++++++++++ .../printer/internal/TokenSourceFragment.java | 51 + .../visitor/printer/internal/TokenType.java | 52 ++ 5 files changed, 1070 insertions(+) create mode 100644 src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java create mode 100644 src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java create mode 100644 src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java create mode 100644 src/main/java/spoon/reflect/visitor/printer/internal/TokenType.java diff --git a/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java index c231dfad8a9..0810b65890b 100644 --- a/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java @@ -17,6 +17,7 @@ package spoon.reflect.visitor.printer; /** + * */ public interface SourceFragment { /** diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java new file mode 100644 index 00000000000..8437d89cffd --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2006-2018 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.reflect.visitor.printer.internal; + +import java.util.List; + +import spoon.reflect.meta.ContainerKind; +import spoon.reflect.path.CtRole; +import spoon.reflect.visitor.printer.SourceFragment; + +/** + * {@link SourceFragment} of List or Set of {@link ElementSourceFragment}s which belongs to collection role. + * For example list of Type members or list of parameters, etc. + * Or set of modifiers and annotations + */ +public class CollectionSourceFragment implements SourceFragment { + + private final List items; + + public CollectionSourceFragment(List items) { + super(); + this.items = items; + } + + @Override + public String getSourceCode() { + StringBuilder sb = new StringBuilder(); + for (SourceFragment childSourceFragment : items) { + sb.append(childSourceFragment.getSourceCode()); + } + return sb.toString(); + } + + /** + * @return child source fragments of this collection + */ + public List getItems() { + return items; + } + + @Override + public String toString() { + return items.toString(); + } + + /** + * @return true if collection contains only children of one role handler with container kind LIST + */ + public boolean isOrdered() { + CtRole role = null; + ContainerKind kind = null; + for (SourceFragment childSourceFragment : items) { + if (childSourceFragment instanceof ElementSourceFragment) { + ElementSourceFragment esf = (ElementSourceFragment) childSourceFragment; + if (role == null) { + role = esf.getRoleInParent(); + kind = esf.getContainerKindInParent(); + if (kind != ContainerKind.LIST) { + return false; + } + } else { + if (role != esf.getRoleInParent()) { + //the collection contains elements of different roles. It cannot be ordered + return false; + } + //else there is another element of the same role - ok + } + } + } + //there are only elements of one role of container kind LIST + return true; + } +} diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java new file mode 100644 index 00000000000..d0ee1049929 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java @@ -0,0 +1,879 @@ +/** + * Copyright (C) 2006-2018 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.reflect.visitor.printer.internal; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import spoon.SpoonException; +import spoon.reflect.code.CtComment; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.cu.CompilationUnit; +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.cu.SourcePositionHolder; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtModifiable; +import spoon.reflect.meta.ContainerKind; +import spoon.reflect.meta.RoleHandler; +import spoon.reflect.meta.impl.RoleHandlerHelper; +import spoon.reflect.path.CtRole; +import spoon.reflect.visitor.CtScanner; +import spoon.reflect.visitor.printer.SourceFragment; +import spoon.support.reflect.CtExtendedModifier; +import spoon.support.reflect.cu.position.SourcePositionImpl; + +/** + * Represents a part of source code of an {@link CtElement} + * It is connected into a tree of {@link ElementSourceFragment}s. + * Use {@link SourcePositionHolder#getOriginalSourceFragment()} to get it. + */ +public class ElementSourceFragment implements SourceFragment { + + /** + * represents an {@link ElementSourceFragment}, which doesn't exist + */ + public static final ElementSourceFragment NO_SOURCE_FRAGMENT = new ElementSourceFragment(null, null); + + private final SourcePositionHolder element; + private final RoleHandler roleHandlerInParent; + private ElementSourceFragment nextSibling; + private ElementSourceFragment firstChild; + + /** + * Creates a source fragment of {@link SourcePositionHolder} + * + * @param element target {@link SourcePositionHolder} + * @param roleHandlerInParent The {@link RoleHandler}, which defines role of target `element` in it's parent + */ + public ElementSourceFragment(SourcePositionHolder element, RoleHandler roleHandlerInParent) { + this.element = element; + this.roleHandlerInParent = roleHandlerInParent; + } + + /** + * @return offset of first character which belongs to this fragment + */ + public int getStart() { + if (firstChild != null) { + return Math.min(getSourcePosition().getSourceStart(), firstChild.getStart()); + } + return getSourcePosition().getSourceStart(); + } + + /** + * @return offset of character after this fragment + */ + public int getEnd() { + if (firstChild != null) { + return Math.max(getSourcePosition().getSourceEnd() + 1, firstChild.getLastSibling().getEnd()); + } + return getSourcePosition().getSourceEnd() + 1; + } + + /** + * @return {@link SourcePosition} of this fragment + */ + public SourcePosition getSourcePosition() { + return element.getPosition(); + } + + @Override + public String toString() { + return "|" + getStart() + ", " + getEnd() + "|" + getSourceCode() + "|"; + } + + /** + * @return origin source code of this fragment + */ + @Override + public String getSourceCode() { + return getSourceCode(getStart(), getEnd()); + } + + /** + * @param start start offset relative to compilation unit + * @param end end offset (after last character) relative to compilation unit + * @return source code of this Fragment between start/end offsets + */ + public String getSourceCode(int start, int end) { + String src = getOriginalSourceCode(); + if (src != null) { + return src.substring(start, end); + } + return null; + } + + /** + * @return true if position points to same compilation unit (source file) as this SourceFragment + */ + private boolean isFromSameSource(SourcePosition position) { + return getSourcePosition().getCompilationUnit().equals(position.getCompilationUnit()); + } + + /** + * Builds tree of {@link SourcePosition}s of `element` and all it's children + * @param element the root element of the tree + */ + public void addTreeOfSourceFragmentsOfElement(CtElement element) { + SourcePosition sp = element.getPosition(); + Deque parents = new ArrayDeque<>(); + parents.push(this); + //scan all children of `element` and build tree of SourceFragments + new CtScanner() { + CtRole scannedRole; + public void scan(CtRole role, CtElement element) { + scannedRole = role; + super.scan(role, element); + } + @Override + protected void enter(CtElement e) { + ElementSourceFragment newFragment = addChild(parents.peek(), scannedRole, e); + if (newFragment != null) { + parents.push(newFragment); + if (e instanceof CtModifiable) { + CtModifiable modifiable = (CtModifiable) e; + Set modifiers = modifiable.getExtendedModifiers(); + for (CtExtendedModifier ctExtendedModifier : modifiers) { + addChild(newFragment, CtRole.MODIFIER, ctExtendedModifier); + } + } + } + } + @Override + protected void exit(CtElement e) { + ElementSourceFragment topFragment = parents.peek(); + if (topFragment != null && topFragment.getElement() == e) { + parents.pop(); + } + } + }.scan(element.getRoleInParent(), element); + } + /** + * @param parentFragment the parent {@link ElementSourceFragment}, which will receive {@link ElementSourceFragment} made for `otherElement` + * @param roleInParent the {@link CtRole} of `otherElement` in scope of element of `parentFragment` + * @param otherElement {@link SourcePositionHolder} whose {@link ElementSourceFragment} has to be added to `parentFragment` + * @return new {@link ElementSourceFragment} created for `otherElement` or null if `otherElement` has no source position or doesn't belong to the same compilation unit + */ + private ElementSourceFragment addChild(ElementSourceFragment parentFragment, CtRole roleInParent, SourcePositionHolder otherElement) { + SourcePosition otherSourcePosition = otherElement.getPosition(); + if (otherSourcePosition instanceof SourcePositionImpl && otherSourcePosition.getCompilationUnit() != null) { + SourcePositionImpl childSPI = (SourcePositionImpl) otherSourcePosition; + if (parentFragment.isFromSameSource(otherSourcePosition)) { + ElementSourceFragment otherFragment = new ElementSourceFragment(otherElement, parentFragment.getRoleHandler(roleInParent, otherElement)); + //parent and child are from the same file. So we can connect their positions into one tree + CMP cmp = parentFragment.compare(otherFragment); + if (cmp == CMP.OTHER_IS_CHILD) { + //child belongs under parent - OK + parentFragment.addChild(otherFragment); + return otherFragment; + } else { + if (cmp == CMP.OTHER_IS_AFTER || cmp == CMP.OTHER_IS_BEFORE) { + if (otherElement instanceof CtComment) { + /* + * comments of elements are sometime not included in source position of element. + * because comments are ignored tokens for java compiler, which computes start/end of elements + * Example: + * + * //a comment + * aStatement(); + * + */ + if (otherFragment.getStart() == 0) { + //it is CompilationUnit comment, which is before package and imports, so it doesn't belong to class + //No problem. Simply add comment at correct position into SourceFragment tree, starting from ROOT + addChild(otherFragment); + return otherFragment; + } + //add this child into parent's source fragment and extend that parent source fragment + parentFragment.addChild(otherFragment); + return otherFragment; + } + } + //the source position of child element is not included in source position of parent element + //I (Pavel) am not sure how to handle it, so let's wait until it happens... +// 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 ... + throw new SpoonException("The SourcePosition of elements are not consistent\nparentFragment: " + parentFragment + "\notherFragment: " + otherFragment); + } + } else { + throw new SpoonException("SourcePosition from unexpected compilation unit: " + otherSourcePosition + " expected is: " + parentFragment.getSourcePosition()); + } + } + //do not connect that undefined source position + return null; + } + + private RoleHandler getRoleHandler(CtRole roleInParent, SourcePositionHolder otherElement) { + SourcePositionHolder parent = element; + if (parent instanceof CompilationUnit) { + parent = null; + } + if (parent == null) { + if (otherElement instanceof CtElement) { + parent = ((CtElement) otherElement).getParent(); + } + } + if (parent instanceof CtElement) { + CtElement ele = (CtElement) parent; + return RoleHandlerHelper.getRoleHandler(ele.getClass(), roleInParent); + } + return null; + } + + /** + * adds `other` {@link ElementSourceFragment} into tree of {@link ElementSourceFragment}s represented by this root element + * + * @param other to be added {@link ElementSourceFragment} + * @return new root of the tree of the {@link ElementSourceFragment}s. It can be be this or `other` + */ + public ElementSourceFragment add(ElementSourceFragment 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, merge this and all siblings of `this` as children and siblings of `other` + other.merge(this); + return other; + } + throw new SpoonException("Unexpected compare result: " + cmp); + } + + private void merge(ElementSourceFragment tobeMerged) { + while (tobeMerged != null) { + ElementSourceFragment nextTobeMerged = tobeMerged.getNextSibling(); + //disconnect tobeMerged from nextSiblings before we add it. So it is added individually and not with wrong siblings too + tobeMerged.nextSibling = null; + add(tobeMerged); + tobeMerged = nextTobeMerged; + } + } + + /** + * adds `fragment` as child fragment of this fragment. If child is located before or after this fragment, + * then start/end of this fragment is moved + * @param fragment to be add + */ + public void addChild(ElementSourceFragment fragment) { + if (firstChild == null) { + firstChild = fragment; + } else { + firstChild = firstChild.add(fragment); + } + } + + private void addNextSibling(ElementSourceFragment sibling) { + if (nextSibling == null) { + nextSibling = sibling; + } else { + nextSibling = nextSibling.add(sibling); + } + } + + private ElementSourceFragment getLastSibling() { + ElementSourceFragment lastSibling = this; + while (lastSibling.nextSibling != null) { + lastSibling = lastSibling.nextSibling; + } + return lastSibling; + } + + 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(ElementSourceFragment 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() + "\"]"); + } + + /** + * @return {@link ElementSourceFragment} which belongs to the same parent and is next in the sources + */ + public ElementSourceFragment getNextSibling() { + return nextSibling; + } + + /** + * @return {@link ElementSourceFragment}, which is first child of this fragment + */ + public ElementSourceFragment getFirstChild() { + return firstChild; + } + + /** + * Searches the tree of fragments for the {@link ElementSourceFragment} with expected `element`, + * which contains `start` and `end` source interval. + * It searches in siblings and children of this {@link ElementSourceFragment} recursively. + * @param element the {@link SourcePositionHolder} of fragment it is looking for or null for any element + * @param start the start offset of searched fragment + * @param end the offset of next character after the end of searched fragment + * + * @return {@link ElementSourceFragment} which represents the root of the CtElement whose sources are in interval [start, end] + */ + public ElementSourceFragment getSourceFragmentOf(SourcePositionHolder element, int start, int end) { + int myEnd = getEnd(); + if (myEnd <= start) { + //search in next sibling + if (nextSibling == null) { + return null; + } + return getRootFragmentOfElement(nextSibling.getSourceFragmentOf(element, start, end)); + } + int myStart = getStart(); + if (myStart <= start) { + if (myEnd >= end) { + if (myStart == start && myEnd == end) { + //we have found exact match + if (element != null && getElement() != element) { + if (firstChild == null) { + throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); + } + return firstChild.getSourceFragmentOf(element, start, end); + } + return this; + } + //it is the child + if (firstChild == null) { + if (element != null && getElement() != element) { + throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); + } + return this; + } + ElementSourceFragment child = getRootFragmentOfElement(firstChild.getSourceFragmentOf(element, start, end)); + if (child != null) { + //all children are smaller then this element + return child; + } + //so this fragment is last one which wraps whole element + if (element != null && getElement() != element) { + throw new SpoonException("There is no source fragment for element " + element.getClass() + ". There is one for class " + getElement().getClass()); + } + return this; + } + //start - end overlaps over multiple fragments + throw new SpoonException("Invalid start/end interval. It overlaps multiple fragments."); + } + return null; + } + + private ElementSourceFragment getRootFragmentOfElement(ElementSourceFragment 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. + * May be null + */ + public SourcePositionHolder getElement() { + return element; + } + + /** + * Note: the List of children is flat. The child fragments of collections (parameters, type members, ...) are next to each other. + * @return list of child fragments of this {@link ElementSourceFragment}. + */ + public List getChildrenFragments() { + if (element instanceof CtLiteral) { + return Collections.singletonList(new TokenSourceFragment(getSourceCode(), TokenType.LITERAL)); + } + List children = new ArrayList<>(); + int off = getStart(); + ElementSourceFragment child = getFirstChild(); + while (child != null) { + forEachConstantFragment(off, child.getStart(), cf -> children.add(cf)); + children.add(child); + off = child.getEnd(); + child = child.getNextSibling(); + } + forEachConstantFragment(off, getEnd(), cf -> children.add(cf)); + return children; + } + + /** + * Detects all child fragments of this {@link ElementSourceFragment}. + * Note: the List of children contains one {@link CollectionSourceFragment} for each collection of fragments (parameters, type members, ...). + * Note: the {@link CollectionSourceFragment} may contain a mix of fragments of different roles, when they overlap. + * For example this code contains mix of annotations and modifiers + * public @Deprecated static @Ignored void method() + * @return list of child fragments of this {@link ElementSourceFragment} where fragments, + * which belongs to the same collection are grouped into {@link CollectionSourceFragment} + */ + public List getGroupedChildrenFragments() { + List flatChildren = getChildrenFragments(); + List result = new ArrayList<>(); + int i = 0; + while (i < flatChildren.size()) { + SourceFragment child = flatChildren.get(i); + if (child instanceof TokenSourceFragment) { + result.add(child); + i++; + continue; + } else if (child instanceof ElementSourceFragment) { + ElementSourceFragment esf = (ElementSourceFragment) child; + ContainerKind kind = esf.getContainerKindInParent(); + if (kind == ContainerKind.SINGLE) { + //it is root element or there is always only one child instance in parent + result.add(child); + i++; + continue; + } + //there can be 0, 1 or more items of children of the same role + //search for another element of the same role + Set foundRoles = new HashSet<>(); + foundRoles.add(checkNotNull(esf.getRoleInParent())); + List childrenInSameCollection = new ArrayList<>(); + //but first include prefix whitespace + SourceFragment spaceChild = removeSuffixSpace(result); + if (spaceChild != null) { + childrenInSameCollection.add(spaceChild); + } + childrenInSameCollection.add(esf); + int lastOfSameRole = findIndexOfLastChildTokenOfRoleHandler(flatChildren, i, esf.getRoleInParent()); + //search for other roles in that interval + i++; + while (i <= lastOfSameRole) { + child = flatChildren.get(i); + childrenInSameCollection.add(child); + CtRole role = null; + if (child instanceof ElementSourceFragment) { + ElementSourceFragment esf2 = (ElementSourceFragment) child; + role = esf2.getRoleInParent(); + } + if (role != null && role != CtRole.COMMENT && foundRoles.add(role)) { + //there is another role in same block, search for last one + lastOfSameRole = Math.max(lastOfSameRole, findIndexOfLastChildTokenOfRoleHandler(flatChildren, i + 1, role)); + } + i++; + } + //add suffix space + if (i < flatChildren.size()) { + SourceFragment nextChild = flatChildren.get(i); + if (isSpaceFragment(nextChild)) { + childrenInSameCollection.add(nextChild); + i++; + } + } + result.add(new CollectionSourceFragment(childrenInSameCollection)); + } else { + throw new SpoonException("Unexpected SourceFragment of type " + child.getClass()); + } + } + return result; + } + + private SourceFragment removeSuffixSpace(List list) { + if (list.size() > 0) { + SourceFragment lastChild = list.get(list.size() - 1); + if (isSpaceFragment(lastChild)) { + list.remove(list.size() - 1); + return lastChild; + } + } + return null; + } + + private T checkNotNull(T o) { + if (o == null) { + throw new SpoonException("Unexpected null value"); + } + return o; + } + + private static int findIndexOfLastChildTokenOfRoleHandler(List childFragments, int start, CtRole role) { + return findIndexOfPreviousFragment(childFragments, start, + filter(ElementSourceFragment.class, fragment -> fragment.getRoleInParent() == role)); + } + + private enum CharType { + SPACE, + NON_SPACE; + + static CharType fromChar(char c) { + return Character.isWhitespace(c) ? SPACE : NON_SPACE; + } + } + + private static final Set separators = new HashSet<>(Arrays.asList("->", "::", "...")); + static { + "(){}[];,.:@=<>?&|".chars().forEach(c -> separators.add(new String(Character.toChars(c)))); + } + private static final Set operators = new HashSet<>(Arrays.asList( + "=", + ">", + "<", + "!", + "~", + "?", + ":", + "==", + "<=", + ">=", + "!=", + "&&", + "||", + "++", + "--", + "+", + "-", + "*", + "/", + "&", + "|", + "^", + "%", + "<<", ">>", ">>>", + + "+=", + "-=", + "*=", + "/=", + "&=", + "|=", + "^=", + "%=", + "<<=", + ">>=", + ">>>="/*, + it is handled as keyword here + "instanceof" + */ + )); + + private static final String[] javaKeywordsJoined = new String[] { + "abstract continue for new switch", + "assert default goto package synchronized", + "boolean do if private this", + "break double implements protected throw", + "byte else import public throws", + "case enum instanceof return transient", + "catch extends int short try", + "char final interface static void", + "class finally long strictfp volatile", + "const float native super while"}; + + private static final Set javaKeywords = new HashSet<>(); + static { + for (String str : javaKeywordsJoined) { + StringTokenizer st = new StringTokenizer(str, " "); + while (st.hasMoreTokens()) { + javaKeywords.add(st.nextToken()); + } + } + } + + private static final List matchers = new ArrayList<>(); + static { + separators.forEach(s -> matchers.add(new StringMatcher(s, TokenType.SEPARATOR))); + operators.forEach(s -> matchers.add(new StringMatcher(s, TokenType.OPERATOR))); + } + + /** + * Calls `consumer` once for each constant {@link SourceFragment} found in source code between `start` and `end` + */ + private void forEachConstantFragment(int start, int end, Consumer consumer) { + if (start == end) { + return; + } + if (start > end) { + throw new SpoonException("Inconsistent start/end. Start=" + start + " is greater then End=" + end); + } + String sourceCode = getOriginalSourceCode(); + StringBuffer buff = new StringBuffer(); + CharType lastType = null; + int off = start; + while (off < end) { + char c = sourceCode.charAt(off); + CharType type = CharType.fromChar(c); + if (type != lastType) { + if (lastType != null) { + onCharSequence(lastType, buff, consumer); + buff.setLength(0); + } + lastType = type; + } + buff.append(c); + off++; + } + onCharSequence(lastType, buff, consumer); + } + + private void onCharSequence(CharType type, StringBuffer buff, Consumer consumer) { + if (type == CharType.SPACE) { + consumer.accept(new TokenSourceFragment(buff.toString(), TokenType.SPACE)); + return; + } + char[] str = new char[buff.length()]; + buff.getChars(0, buff.length(), str, 0); + int off = 0; + while (off < str.length) { + //detect java identifier or keyword + int lenOfIdentifier = detectJavaIdentifier(str, off); + if (lenOfIdentifier > 0) { + String identifier = new String(str, off, lenOfIdentifier); + if (javaKeywords.contains(identifier)) { + //it is a java keyword + consumer.accept(new TokenSourceFragment(identifier, TokenType.KEYWORD)); + } else { + //it is a java identifier + consumer.accept(new TokenSourceFragment(identifier, TokenType.IDENTIFIER)); + } + off += lenOfIdentifier; + continue; + } + //detect longest match in matchers + StringMatcher longestMatcher = null; + for (StringMatcher strMatcher : matchers) { + if (strMatcher.isMatch(str, off)) { + longestMatcher = strMatcher.getLonger(longestMatcher); + } + } + if (longestMatcher == null) { + throw new SpoonException("Unexpected source text: " + buff.toString()); + } + consumer.accept(new TokenSourceFragment(longestMatcher.toString(), longestMatcher.getType())); + off += longestMatcher.getLength(); + } + } + + /** + * @return number of characters in buff starting from start which are java identifier + */ + private int detectJavaIdentifier(char[] buff, int start) { + int len = buff.length; + int o = start; + if (start <= len) { + char c = buff[o]; + if (Character.isJavaIdentifierStart(c)) { + o++; + while (o < len) { + c = buff[o]; + if (Character.isJavaIdentifierPart(c) == false) { + break; + } + o++; + } + } + } + return o - start; + } + + private String getOriginalSourceCode() { + CompilationUnit cu = getSourcePosition().getCompilationUnit(); + if (cu != null) { + return cu.getOriginalSourceCode(); + } + return null; + } + + private static final class StringMatcher { + private final TokenType type; + private final char[] chars; + + private StringMatcher(final String str, TokenType type) { + super(); + this.type = type; + chars = str.toCharArray(); + } + + public boolean isMatch(final char[] buffer, int pos) { + final int len = chars.length; + if (pos + len > buffer.length) { + return false; + } + for (int i = 0; i < chars.length; i++, pos++) { + if (chars[i] != buffer[pos]) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return new String(chars); + } + + public int getLength() { + return chars.length; + } + + public StringMatcher getLonger(StringMatcher m) { + if (m != null && m.getLength() > getLength()) { + return m; + } + return this; + } + + public TokenType getType() { + return type; + } + } + + /** + * @return role of the element of this fragment in scope of it's parent + */ + public CtRole getRoleInParent() { + return roleHandlerInParent != null ? roleHandlerInParent.getRole() : null; + } + + /** + * @return the {@link ContainerKind} of the attribute which holds the element of this fragment in it's parent + */ + public ContainerKind getContainerKindInParent() { + if (roleHandlerInParent != null) { + if (roleHandlerInParent.getRole() != CtRole.COMMENT) { + return roleHandlerInParent.getContainerKind(); + } + } + return ContainerKind.SINGLE; + } + /** + * looks for next fragment whose {@link Predicate} `test` returns true + * @param start - the index of first to be checked fragment + * @return index of found fragment, or -1 if not found + */ + static int findIndexOfNextFragment(List fragments, int start, Predicate test) { + while (start < fragments.size()) { + SourceFragment fragment = fragments.get(start); + if (test.test(fragment)) { + return start; + } + start++; + } + return -1; + } + + /** + * @param start the index of element with lower index which is checked and may be returned + * @param test a {@link Predicate}, which is evaluated for each item of `fragments` starting from last one and ending with item in index `start` + * @return index of found fragment, or -1 if not found + */ + static int findIndexOfPreviousFragment(List fragments, int start, Predicate test) { + int i = fragments.size() - 1; + while (i >= start) { + if (test.test(fragments.get(i))) { + return i; + } + i--; + } + return -1; + } + + /** + * @param predicate the {@link Predicate}, which has to be checkd for each item of {@link CollectionSourceFragment} + * @return {@link Predicate} which calls `predicate` for each item of {@link CollectionSourceFragment} + * Returned {@link Predicate} returns true only if `predicate` returns true on at least one item + */ + static Predicate checkCollectionItems(Predicate predicate) { + return (SourceFragment fragment) -> { + if (fragment instanceof CollectionSourceFragment) { + CollectionSourceFragment collectionFragment = (CollectionSourceFragment) fragment; + for (SourceFragment itemFragment : collectionFragment.getItems()) { + if (predicate.test(itemFragment)) { + return true; + } + } + return false; + } else { + return predicate.test(fragment); + } + }; + } + + /** + * @param predicate to be called {@link Predicate} + * @return {@link Predicate} which calls `predicate` only for {@link SourceFragment}s of of type `clazz` and returns false for others + */ + static Predicate filter(Class clazz, Predicate predicate) { + return fragment -> { + if (clazz.isInstance(fragment)) { + return predicate.test((T) fragment); + } + return false; + }; + } + + /** + * @return true if {@link SourceFragment} represents a white space + */ + static boolean isSpaceFragment(SourceFragment fragment) { + return fragment instanceof TokenSourceFragment && ((TokenSourceFragment) fragment).getType() == TokenType.SPACE; + } +} diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java new file mode 100644 index 00000000000..e090ae5a74a --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2006-2018 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.reflect.visitor.printer.internal; + +import spoon.reflect.visitor.printer.SourceFragment; + +/** + * a {@link SourceFragment} of some primitive String token. + */ +public class TokenSourceFragment implements SourceFragment { + + private final String source; + private final TokenType type; + + public TokenSourceFragment(String source, TokenType type) { + super(); + this.source = source; + this.type = type; + } + + @Override + public String getSourceCode() { + return source; + } + + /** + * @return type of token of this fragment + */ + public TokenType getType() { + return type; + } + + @Override + public String toString() { + return "|" + getSourceCode() + "|"; + } +} diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/TokenType.java b/src/main/java/spoon/reflect/visitor/printer/internal/TokenType.java new file mode 100644 index 00000000000..a2edf0adf77 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/internal/TokenType.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2006-2018 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.reflect.visitor.printer.internal; + +import spoon.reflect.visitor.TokenWriter; + +/** + * Type of {@link TokenSourceFragment} token. + * Note: These types mirrors the methods of {@link TokenWriter} + */ +public enum TokenType { + + SEPARATOR(false, false), + OPERATOR(false, false), + LITERAL(false, false), + KEYWORD(false, false), + IDENTIFIER(false, false), + CODE_SNIPPET(false, false), + COMMENT(false, false), + NEW_LINE(true, false), + INC_TAB(true, true), + DEC_TAB(true, true), + SPACE(true, false); + + private final boolean whiteSpace; + private final boolean tab; + + TokenType(boolean whiteSpace, boolean tab) { + this.whiteSpace = whiteSpace; + this.tab = tab; + } + boolean isWhiteSpace() { + return whiteSpace; + } + public boolean isTab() { + return tab; + } +} From ec98d9f24fad3ac49d6e33701c5f64e06c861883 Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Thu, 6 Sep 2018 22:42:51 +0200 Subject: [PATCH 12/16] up --- .../spoon/test/architecture/SpoonArchitectureEnforcerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java index f557250bea4..5b98bb19b77 100644 --- a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java +++ b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java @@ -365,7 +365,7 @@ public void testSpecPackage() { officialPackages.add("spoon.reflect.visitor.chain"); officialPackages.add("spoon.reflect.visitor.filter"); officialPackages.add("spoon.reflect.visitor.printer"); - officialPackages.add("spoon.reflect.visitor.printer.change"); + officialPackages.add("spoon.reflect.visitor.printer.internal"); officialPackages.add("spoon.reflect.visitor"); officialPackages.add("spoon.reflect"); officialPackages.add("spoon.support.comparator"); From 19acd2cad09296446eadd3c821e09ea9eb9e92bf Mon Sep 17 00:00:00 2001 From: Pavel Vojtechovsky Date: Mon, 10 Sep 2018 21:03:05 +0200 Subject: [PATCH 13/16] getOriginalSourceFragment returns ElementSourceFragment, @experimental --- src/main/java/spoon/reflect/cu/SourcePositionHolder.java | 3 +-- .../java/spoon/reflect/visitor/printer/SourceFragment.java | 5 ++++- .../visitor/printer/internal/CollectionSourceFragment.java | 4 +++- .../visitor/printer/internal/ElementSourceFragment.java | 2 ++ .../visitor/printer/internal/TokenSourceFragment.java | 5 ++++- src/main/java/spoon/support/reflect/CtExtendedModifier.java | 2 +- .../spoon/support/reflect/declaration/CtElementImpl.java | 2 +- src/test/java/spoon/test/main/MainTest.java | 2 +- src/test/java/spoon/test/position/TestSourceFragment.java | 2 +- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java index 6567b74f67a..92f76176f33 100644 --- a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java +++ b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java @@ -16,7 +16,6 @@ */ package spoon.reflect.cu; -import spoon.reflect.visitor.printer.SourceFragment; import spoon.reflect.visitor.printer.internal.ElementSourceFragment; /** @@ -30,7 +29,7 @@ public interface SourcePositionHolder { * Returns the original source code (maybe different from toString() if a transformation has been applied. * Or {@link ElementSourceFragment#NO_SOURCE_FRAGMENT} if this element has no original source fragment. */ - default SourceFragment getOriginalSourceFragment() { + default ElementSourceFragment getOriginalSourceFragment() { return ElementSourceFragment.NO_SOURCE_FRAGMENT; } } diff --git a/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java index 0810b65890b..5514f1640cb 100644 --- a/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java @@ -16,9 +16,12 @@ */ package spoon.reflect.visitor.printer; +import spoon.support.Experimental; + /** - * + * Represents a part of source code */ +@Experimental public interface SourceFragment { /** * @return origin source code of whole fragment represented by this instance diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java index 8437d89cffd..4968e23b904 100644 --- a/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java @@ -21,12 +21,14 @@ import spoon.reflect.meta.ContainerKind; import spoon.reflect.path.CtRole; import spoon.reflect.visitor.printer.SourceFragment; +import spoon.support.Experimental; /** - * {@link SourceFragment} of List or Set of {@link ElementSourceFragment}s which belongs to collection role. + * {@link SourceFragment} of List or Set of {@link ElementSourceFragment}s which belong to collection role. * For example list of Type members or list of parameters, etc. * Or set of modifiers and annotations */ +@Experimental public class CollectionSourceFragment implements SourceFragment { private final List items; diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java index d0ee1049929..00855b76b19 100644 --- a/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java @@ -42,6 +42,7 @@ import spoon.reflect.path.CtRole; import spoon.reflect.visitor.CtScanner; import spoon.reflect.visitor.printer.SourceFragment; +import spoon.support.Experimental; import spoon.support.reflect.CtExtendedModifier; import spoon.support.reflect.cu.position.SourcePositionImpl; @@ -50,6 +51,7 @@ * It is connected into a tree of {@link ElementSourceFragment}s. * Use {@link SourcePositionHolder#getOriginalSourceFragment()} to get it. */ +@Experimental public class ElementSourceFragment implements SourceFragment { /** diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java index e090ae5a74a..b68053bc043 100644 --- a/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java @@ -17,10 +17,13 @@ package spoon.reflect.visitor.printer.internal; import spoon.reflect.visitor.printer.SourceFragment; +import spoon.support.Experimental; /** - * a {@link SourceFragment} of some primitive String token. + * a {@link SourceFragment} of some primitive String token, + * like separator, operator, whitespace, ... */ +@Experimental public class TokenSourceFragment implements SourceFragment { private final String source; diff --git a/src/main/java/spoon/support/reflect/CtExtendedModifier.java b/src/main/java/spoon/support/reflect/CtExtendedModifier.java index 6da4a2f7cef..8af784d6622 100644 --- a/src/main/java/spoon/support/reflect/CtExtendedModifier.java +++ b/src/main/java/spoon/support/reflect/CtExtendedModifier.java @@ -95,7 +95,7 @@ public ElementSourceFragment getOriginalSourceFragment() { SourcePosition sp = this.getPosition(); CompilationUnit compilationUnit = sp.getCompilationUnit(); if (compilationUnit != null) { - ElementSourceFragment rootFragment = (ElementSourceFragment) compilationUnit.getOriginalSourceFragment(); + ElementSourceFragment rootFragment = compilationUnit.getOriginalSourceFragment(); return rootFragment.getSourceFragmentOf(this, sp.getSourceStart(), sp.getSourceEnd() + 1); } else { return ElementSourceFragment.NO_SOURCE_FRAGMENT; diff --git a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java index 0ec5ed1e78d..8d58c0fb85f 100644 --- a/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java +++ b/src/main/java/spoon/support/reflect/declaration/CtElementImpl.java @@ -620,7 +620,7 @@ public ElementSourceFragment getOriginalSourceFragment() { SourcePosition sp = this.getPosition(); CompilationUnit compilationUnit = sp.getCompilationUnit(); if (compilationUnit != null) { - ElementSourceFragment rootFragment = (ElementSourceFragment) compilationUnit.getOriginalSourceFragment(); + ElementSourceFragment rootFragment = compilationUnit.getOriginalSourceFragment(); return rootFragment.getSourceFragmentOf(this, sp.getSourceStart(), sp.getSourceEnd() + 1); } else { return ElementSourceFragment.NO_SOURCE_FRAGMENT; diff --git a/src/test/java/spoon/test/main/MainTest.java b/src/test/java/spoon/test/main/MainTest.java index 1927f9999ec..9201e3f5aa4 100644 --- a/src/test/java/spoon/test/main/MainTest.java +++ b/src/test/java/spoon/test/main/MainTest.java @@ -487,7 +487,7 @@ public void testSourcePositionTreeIsCorrectlyOrdered() { boolean hasComment = false; for (CtType type : types) { SourcePosition sp = type.getPosition(); - totalCount += assertSourcePositionTreeIsCorrectlyOrder((ElementSourceFragment) sp.getCompilationUnit().getOriginalSourceFragment(), 0, sp.getCompilationUnit().getOriginalSourceCode().length()); + totalCount += assertSourcePositionTreeIsCorrectlyOrder(sp.getCompilationUnit().getOriginalSourceFragment(), 0, sp.getCompilationUnit().getOriginalSourceCode().length()); hasComment = hasComment || type.getComments().size() > 0; }; assertTrue(totalCount > 1000); diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java index b53ac4251fd..2b9da167928 100644 --- a/src/test/java/spoon/test/position/TestSourceFragment.java +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -179,7 +179,7 @@ public void testExactSourceFragments() throws Exception { } private void checkElementFragments(CtElement ele, Object... expectedFragments) { - ElementSourceFragment fragment = (ElementSourceFragment) ele.getOriginalSourceFragment(); + ElementSourceFragment fragment = ele.getOriginalSourceFragment(); List children = fragment.getChildrenFragments(); assertEquals(expandGroup(new ArrayList<>(), expectedFragments), childSourceFragmentsToStrings(children)); assertGroupsEqual(expectedFragments, fragment.getGroupedChildrenFragments()); From e6071dfaeb6f3d9c3aca3d803d638f1f93700dba Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Mon, 10 Sep 2018 21:48:46 +0200 Subject: [PATCH 14/16] up --- chore/check-links-in-doc.py | 2 +- .../spoon/test/position/TestSourceFragment.java | 13 ++++++++++--- .../testclasses/MethodWithJavaDocAndModifiers.java | 12 ------------ 3 files changed, 11 insertions(+), 16 deletions(-) delete mode 100644 src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java diff --git a/chore/check-links-in-doc.py b/chore/check-links-in-doc.py index b257f5b2418..29c6cc855db 100644 --- a/chore/check-links-in-doc.py +++ b/chore/check-links-in-doc.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.7 +#!/usr/bin/python # checks the links of the Spoon documentation # a problem is reported as an exception, hence as a Unic return code != -1, hence as a build failure diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java index 2b9da167928..266c78e5857 100644 --- a/src/test/java/spoon/test/position/TestSourceFragment.java +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -1,8 +1,10 @@ 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 static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Arrays; @@ -154,6 +156,8 @@ public void testExactSourceFragments() throws Exception { Factory f = comp.getFactory(); final CtType foo = f.Type().get(FooSourceFragments.class); + + // contract: the fragment returned by getOriginalSourceFragment are correct checkElementFragments(foo.getMethodsByName("m1").get(0).getBody().getStatement(0), "if", "(", "x > 0", ")", "{this.getClass();}", "else", "{/*empty*/}"); checkElementFragments(foo.getMethodsByName("m2").get(0).getBody().getStatement(0), @@ -181,7 +185,10 @@ public void testExactSourceFragments() throws Exception { private void checkElementFragments(CtElement ele, Object... expectedFragments) { ElementSourceFragment fragment = ele.getOriginalSourceFragment(); List children = fragment.getChildrenFragments(); - assertEquals(expandGroup(new ArrayList<>(), expectedFragments), childSourceFragmentsToStrings(children)); + + // calls getSourceCode on on elements of children + assertEquals(expandGroup(new ArrayList<>(), expectedFragments), toCodeStrings(children)); + assertGroupsEqual(expectedFragments, fragment.getGroupedChildrenFragments()); } @@ -210,13 +217,13 @@ private static void assertGroupsEqual(Object[] expectedFragments, List { if (item instanceof CollectionSourceFragment) { CollectionSourceFragment csf = (CollectionSourceFragment) item; - return "group("+childSourceFragmentsToStrings(csf.getItems()).toString() + ")"; + return "group("+ toCodeStrings(csf.getItems()).toString() + ")"; } return item.getSourceCode(); }).collect(Collectors.toList())); } - private static List childSourceFragmentsToStrings(List csf) { + private static List toCodeStrings(List csf) { return csf.stream().map(SourceFragment::getSourceCode).collect(Collectors.toList()); } } diff --git a/src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java b/src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java deleted file mode 100644 index 56c9dec7c9f..00000000000 --- a/src/test/java/spoon/test/position/testclasses/MethodWithJavaDocAndModifiers.java +++ /dev/null @@ -1,12 +0,0 @@ -package spoon.test.position.testclasses; - -public class MethodWithJavaDocAndModifiers { - - /** - * Method with javadoc - * @param parm1 the parameter - */ - public @Deprecated int mWithDoc(int parm1) { - return parm1; - } -} \ No newline at end of file From 7a8c1f4a896c5dbea972c2ef161970b91d259e4d Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Tue, 11 Sep 2018 15:53:37 +0200 Subject: [PATCH 15/16] up --- .../spoon/reflect/cu/CompilationUnit.java | 2 +- .../reflect/cu/SourcePositionHolder.java | 5 ++++ .../visitor/printer/SourceFragment.java | 30 ------------------- .../internal/CollectionSourceFragment.java | 1 - .../internal/ElementSourceFragment.java | 25 ++++++++-------- .../printer/internal/TokenSourceFragment.java | 1 - .../test/position/TestSourceFragment.java | 2 +- 7 files changed, 19 insertions(+), 47 deletions(-) delete mode 100644 src/main/java/spoon/reflect/visitor/printer/SourceFragment.java diff --git a/src/main/java/spoon/reflect/cu/CompilationUnit.java b/src/main/java/spoon/reflect/cu/CompilationUnit.java index cfd1d14ba35..0676afcfe54 100644 --- a/src/main/java/spoon/reflect/cu/CompilationUnit.java +++ b/src/main/java/spoon/reflect/cu/CompilationUnit.java @@ -17,10 +17,10 @@ package spoon.reflect.cu; import spoon.processing.FactoryAccessor; +import spoon.reflect.declaration.CtImport; import spoon.reflect.declaration.CtModule; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; -import spoon.reflect.declaration.CtImport; import spoon.support.Experimental; import java.io.File; diff --git a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java index 92f76176f33..7677a182081 100644 --- a/src/main/java/spoon/reflect/cu/SourcePositionHolder.java +++ b/src/main/java/spoon/reflect/cu/SourcePositionHolder.java @@ -17,6 +17,7 @@ package spoon.reflect.cu; import spoon.reflect.visitor.printer.internal.ElementSourceFragment; +import spoon.support.Experimental; /** * This interface represents an element which knows its position in a source file. @@ -28,7 +29,11 @@ public interface SourcePositionHolder { /** * Returns the original source code (maybe different from toString() if a transformation has been applied. * Or {@link ElementSourceFragment#NO_SOURCE_FRAGMENT} if this element has no original source fragment. + * + * Warning: this is a advanced method which cannot be considered as part of the stable API + * */ + @Experimental default ElementSourceFragment getOriginalSourceFragment() { return ElementSourceFragment.NO_SOURCE_FRAGMENT; } diff --git a/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java deleted file mode 100644 index 5514f1640cb..00000000000 --- a/src/main/java/spoon/reflect/visitor/printer/SourceFragment.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (C) 2006-2018 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.reflect.visitor.printer; - -import spoon.support.Experimental; - -/** - * Represents a part of source code - */ -@Experimental -public interface SourceFragment { - /** - * @return origin source code of whole fragment represented by this instance - */ - String getSourceCode(); -} diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java index 4968e23b904..06f7aa5ca95 100644 --- a/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/internal/CollectionSourceFragment.java @@ -20,7 +20,6 @@ import spoon.reflect.meta.ContainerKind; import spoon.reflect.path.CtRole; -import spoon.reflect.visitor.printer.SourceFragment; import spoon.support.Experimental; /** diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java index 00855b76b19..ac4be63ff1c 100644 --- a/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/internal/ElementSourceFragment.java @@ -16,18 +16,6 @@ */ package spoon.reflect.visitor.printer.internal; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Deque; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.function.Consumer; -import java.util.function.Predicate; - import spoon.SpoonException; import spoon.reflect.code.CtComment; import spoon.reflect.code.CtLiteral; @@ -41,11 +29,22 @@ import spoon.reflect.meta.impl.RoleHandlerHelper; import spoon.reflect.path.CtRole; import spoon.reflect.visitor.CtScanner; -import spoon.reflect.visitor.printer.SourceFragment; import spoon.support.Experimental; import spoon.support.reflect.CtExtendedModifier; import spoon.support.reflect.cu.position.SourcePositionImpl; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.function.Consumer; +import java.util.function.Predicate; + /** * Represents a part of source code of an {@link CtElement} * It is connected into a tree of {@link ElementSourceFragment}s. diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java index b68053bc043..dbdea67eb9a 100644 --- a/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java +++ b/src/main/java/spoon/reflect/visitor/printer/internal/TokenSourceFragment.java @@ -16,7 +16,6 @@ */ package spoon.reflect.visitor.printer.internal; -import spoon.reflect.visitor.printer.SourceFragment; import spoon.support.Experimental; /** diff --git a/src/test/java/spoon/test/position/TestSourceFragment.java b/src/test/java/spoon/test/position/TestSourceFragment.java index 266c78e5857..b5227987026 100644 --- a/src/test/java/spoon/test/position/TestSourceFragment.java +++ b/src/test/java/spoon/test/position/TestSourceFragment.java @@ -22,7 +22,7 @@ import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtType; import spoon.reflect.factory.Factory; -import spoon.reflect.visitor.printer.SourceFragment; +import spoon.reflect.visitor.printer.internal.SourceFragment; import spoon.reflect.visitor.printer.internal.CollectionSourceFragment; import spoon.reflect.visitor.printer.internal.ElementSourceFragment; import spoon.support.reflect.cu.CompilationUnitImpl; From a9323985c44afc67d37cfd7e303f2c8974436965 Mon Sep 17 00:00:00 2001 From: Martin Monperrus Date: Tue, 11 Sep 2018 16:14:25 +0200 Subject: [PATCH 16/16] up --- .../printer/internal/SourceFragment.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/spoon/reflect/visitor/printer/internal/SourceFragment.java diff --git a/src/main/java/spoon/reflect/visitor/printer/internal/SourceFragment.java b/src/main/java/spoon/reflect/visitor/printer/internal/SourceFragment.java new file mode 100644 index 00000000000..5046882cc36 --- /dev/null +++ b/src/main/java/spoon/reflect/visitor/printer/internal/SourceFragment.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2006-2018 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.reflect.visitor.printer.internal; + +import spoon.support.Experimental; + +/** + * Represents a part of source code. + * + * See https://github.com/INRIA/spoon/pull/2283 + */ +@Experimental +public interface SourceFragment { + /** + * @return origin source code of whole fragment represented by this instance + */ + String getSourceCode(); +}