diff --git a/src/main/java/spoon/pattern/AbstractAttributeSubstitutionRequest.java b/src/main/java/spoon/pattern/AbstractAttributeSubstitutionRequest.java new file mode 100644 index 00000000000..9b92630360b --- /dev/null +++ b/src/main/java/spoon/pattern/AbstractAttributeSubstitutionRequest.java @@ -0,0 +1,61 @@ +/** + * 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.pattern; + +import java.util.Map; + +import spoon.reflect.declaration.CtElement; +import spoon.reflect.path.CtRole; +import spoon.template.TemplateMatcher; + +/** + * Represents the request for substitution of an attribute of AST node by different value. + * For example: CtModifiable.modifiers, CtBinaryOperator.leftHandOperand, ... + * Whole values of CtNamedElement.simpleName, CtStatement.label, CtComment.comment + * All statements of CtBlock, ... + */ +public abstract class AbstractAttributeSubstitutionRequest implements SubstitutionRequest { + private final NodeAttributesSubstitionRequest owner; + private final CtRole roleOfSubstitutedValue; + + + AbstractAttributeSubstitutionRequest(NodeAttributesSubstitionRequest owner, CtRole roleOfSubstitutedValue) { + this.owner = owner; + this.roleOfSubstitutedValue = roleOfSubstitutedValue; + } + + public NodeAttributesSubstitionRequest getOwner() { + return owner; + } + + @Override + public CtRole getRoleOfSubstitutedValue() { + return roleOfSubstitutedValue; + } + + public CtElement getSubstitutedNode() { + return owner.getSubstitutedNode(); + } + + @Override + public CtElement getParentNode() { + return owner.getSubstitutedNode(); + } + + public abstract void substitute(T clonedElement, Map params); + public abstract boolean matches(TemplateMatcher matcher, Object target, Object template); +} diff --git a/src/main/java/spoon/pattern/AbstractNodeSubstitutionRequest.java b/src/main/java/spoon/pattern/AbstractNodeSubstitutionRequest.java new file mode 100644 index 00000000000..abf520aee57 --- /dev/null +++ b/src/main/java/spoon/pattern/AbstractNodeSubstitutionRequest.java @@ -0,0 +1,52 @@ +/** + * 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.pattern; + +import java.util.List; +import java.util.Map; + +import spoon.pattern.Pattern.ModelCloner; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.path.CtRole; + +/** + * Represents the generic request for some substitution of Pattern model node + */ +public abstract class AbstractNodeSubstitutionRequest implements SubstitutionRequest { + + private final CtElement substitutedNode; + + + AbstractNodeSubstitutionRequest(CtElement substitutedNode) { + super(); + this.substitutedNode = substitutedNode; + } + + public CtElement getSubstitutedNode() { + return substitutedNode; + } + + public CtRole getRoleOfSubstitutedValue() { + return getSubstitutedNode().getRoleInParent(); + } + + public CtElement getParentNode() { + return getSubstitutedNode().getParent(); + } + + public abstract List substitute(ModelCloner cloneHelper, Map params); +} diff --git a/src/main/java/spoon/pattern/AttributeSubstitutionRequest.java b/src/main/java/spoon/pattern/AttributeSubstitutionRequest.java new file mode 100644 index 00000000000..6717919aa8b --- /dev/null +++ b/src/main/java/spoon/pattern/AttributeSubstitutionRequest.java @@ -0,0 +1,66 @@ +/** + * 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.pattern; + +import java.util.List; +import java.util.Map; + +import spoon.reflect.declaration.CtElement; +import spoon.reflect.meta.ContainerKind; +import spoon.reflect.path.CtRole; +import spoon.template.TemplateMatcher; + +/** + * Represents the request for substitution of an attribute of AST node by different value. + * For example: CtModifiable.modifiers, CtBinaryOperator.leftHandOperand, ... + * Whole values of CtNamedElement.simpleName, CtStatement.label, CtComment.comment + * All statements of CtBlock, ... + */ +public class AttributeSubstitutionRequest extends AbstractAttributeSubstitutionRequest { + + private final ParameterInfo parameterInfo; + + AttributeSubstitutionRequest(NodeAttributesSubstitionRequest owner, ParameterInfo parameterInfo, CtRole roleOfSubstitutedValue) { + super(owner, roleOfSubstitutedValue); + this.parameterInfo = parameterInfo; + this.parameterInfo.addSubstitutionRequest(this); + } + + public ParameterInfo getParameterInfo() { + return parameterInfo; + } + + @Override + public void substitute(T clonedElement, Map params) { + ConversionContext context = new ConversionContext(this, getParameterInfo(), params); + List convertedValues = getParameterInfo().getValueConvertor().getValueAs(context); + if (context.getRoleHandler().getContainerKind() == ContainerKind.SINGLE) { + if (convertedValues.isEmpty()) { + context.getRoleHandler().setValue(clonedElement, null); + } else { + context.getRoleHandler().setValue(clonedElement, convertedValues.get(0)); + } + } else { + context.getRoleHandler().setValue(clonedElement, convertedValues); + } + } + + @Override + public boolean matches(TemplateMatcher matcher, Object target, Object template) { + return matcher.addMatch(getParameterInfo(), target); + } +} diff --git a/src/main/java/spoon/pattern/ConversionContext.java b/src/main/java/spoon/pattern/ConversionContext.java new file mode 100644 index 00000000000..fef3f632266 --- /dev/null +++ b/src/main/java/spoon/pattern/ConversionContext.java @@ -0,0 +1,85 @@ +/** + * 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.pattern; + +import java.util.Map; + +import spoon.SpoonException; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.meta.RoleHandler; +import spoon.reflect.meta.impl.RoleHandlerHelper; +import spoon.reflect.path.CtRole; + +/** + * Converts the individual parameter values to required type + */ +public class ConversionContext { + private final SubstitutionRequest substitutionRequest; + private final Object value; + private Class requiredClass; + private final CtRole roleInParent; + private final CtElement parent; + private final RoleHandler roleHandler; + private final ParameterInfo parameterInfo; + + public ConversionContext(SubstitutionRequest substitutionRequest, ParameterInfo parameterInfo, Map params) { + this.value = params.get(parameterInfo.getName()); + this.substitutionRequest = substitutionRequest; + this.parameterInfo = parameterInfo; + this.roleInParent = substitutionRequest.getRoleOfSubstitutedValue(); + this.parent = substitutionRequest.getParentNode(); + this.roleHandler = RoleHandlerHelper.getRoleHandler(parent.getClass(), roleInParent); + } + + public SubstitutionRequest getSubstitutionRequest() { + return substitutionRequest; + } + + public Object getValue() { + return value; + } + + public CtRole getRoleInParent() { + return roleInParent; + } + + public CtElement getParent() { + return parent; + } + + public RoleHandler getRoleHandler() { + return roleHandler; + } + + public ParameterInfo getParameterInfo() { + return parameterInfo; + } + + public Class getRequiredClass() { + if (requiredClass == null) { + return roleHandler.getValueClass(); + } + return requiredClass; + } + + public void setRequiredClass(Class requiredClass) { + if (roleHandler.getValueClass().isAssignableFrom(requiredClass) == false) { + throw new SpoonException("Cannot force required type to " + requiredClass.getName() + " for Role of type " + roleHandler.getValueClass().getName()); + } + this.requiredClass = requiredClass; + } +} diff --git a/src/main/java/spoon/pattern/NodeAttributesSubstitionRequest.java b/src/main/java/spoon/pattern/NodeAttributesSubstitionRequest.java new file mode 100644 index 00000000000..a883728b468 --- /dev/null +++ b/src/main/java/spoon/pattern/NodeAttributesSubstitionRequest.java @@ -0,0 +1,65 @@ +/** + * 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.pattern; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import spoon.SpoonException; +import spoon.pattern.Pattern.ModelCloner; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.path.CtRole; + +/** + * Represents the request for substitution of attributes of node of Pattern model + */ +public class NodeAttributesSubstitionRequest extends AbstractNodeSubstitutionRequest { + + private final Map attributeSubstititionRequests = new HashMap<>(); + + + NodeAttributesSubstitionRequest(CtElement substitutedNode) { + super(substitutedNode); + } + + void addAttributeSubstitutionRequest(AbstractAttributeSubstitutionRequest sr) { + AbstractAttributeSubstitutionRequest old = attributeSubstititionRequests.put(sr.getRoleOfSubstitutedValue(), sr); + if (old != null && old != sr) { + throw new SpoonException("Cannot add two substitution requests to the same node attribute " + sr.getRoleOfSubstitutedValue()); + } + } + + public Map getAttributeSubstititionRequests() { + return Collections.unmodifiableMap(attributeSubstititionRequests); + } + + public AbstractAttributeSubstitutionRequest getAttributeSubstititionRequest(CtRole attributeRole) { + return attributeSubstititionRequests.get(attributeRole); + } + + @Override + public List substitute(ModelCloner cloneHelper, Map params) { + @SuppressWarnings("unchecked") + T clonedElement = (T) cloneHelper.originClone(getSubstitutedNode()); + for (AbstractAttributeSubstitutionRequest attributeSubstitutionRequest : attributeSubstititionRequests.values()) { + attributeSubstitutionRequest.substitute(clonedElement, params); + } + return Collections.singletonList(clonedElement); + } +} diff --git a/src/main/java/spoon/pattern/NodeSubstitutionRequest.java b/src/main/java/spoon/pattern/NodeSubstitutionRequest.java new file mode 100644 index 00000000000..a5d1e5c4fd3 --- /dev/null +++ b/src/main/java/spoon/pattern/NodeSubstitutionRequest.java @@ -0,0 +1,69 @@ +/** + * 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.pattern; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import spoon.pattern.Pattern.ModelCloner; +import spoon.reflect.declaration.CtElement; +import spoon.template.TemplateMatcher; + +/** + * Represents the request for substitution of one value in Pattern model + */ +public class NodeSubstitutionRequest extends AbstractNodeSubstitutionRequest { + + private final ParameterInfo parameterInfo; + + + NodeSubstitutionRequest(ParameterInfo parameterInfo, CtElement substitutedNode) { + super(substitutedNode); + this.parameterInfo = parameterInfo; + this.parameterInfo.addSubstitutionRequest(this); + } + + /** + * @return link to {@link Pattern} parameter + */ + public ParameterInfo getParameterInfo() { + return parameterInfo; + } + + /** + * Substitutes getSubstitutedNode() by the cloned value converted to the type desired by CtRole of getSubstitutedNode().getParent() + */ + @SuppressWarnings("unchecked") + @Override + public List substitute(ModelCloner cloneHelper, Map params) { + ConversionContext context = new ConversionContext(this, getParameterInfo(), params); + if (context.getValue() == null) { + return Collections.emptyList(); + } + return getParameterInfo().getValueConvertor().getValueAs(context); + } + + public boolean matches(TemplateMatcher matcher, Object target, CtElement template) { + return matcher.addMatch(getParameterInfo(), target); + } + + @Override + public String toString() { + return "Replace " + getSubstitutedNode() + " in " + getSubstitutedNode().getParent() + "." + getSubstitutedNode().getRoleInParent().getCamelCaseName(); + } +} diff --git a/src/main/java/spoon/pattern/ParameterInfo.java b/src/main/java/spoon/pattern/ParameterInfo.java new file mode 100644 index 00000000000..854efab4a26 --- /dev/null +++ b/src/main/java/spoon/pattern/ParameterInfo.java @@ -0,0 +1,89 @@ +/** + * 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.pattern; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import spoon.reflect.declaration.CtElement; +import spoon.support.template.ParameterMatcher; +import spoon.template.TemplateMatcher; + +/** + * Represents the parameter of {@link Pattern} + */ +public class ParameterInfo { + private final Pattern pattern; + /** + * uniquely identifies parameter in scope of owner {@link Pattern} + */ + private final String name; + private final List substitutionRequests = new ArrayList<>(); + private List matchConditions; + private ValueConvertor valueConvertor; + + ParameterInfo(Pattern pattern, String name) { + super(); + this.pattern = pattern; + this.name = name; + } + + public String getName() { + return name; + } + + void addSubstitutionRequest(SubstitutionRequest substrRequest) { + substitutionRequests.add(substrRequest); + } + + void addMatchCondition(Predicate matchCondition) { + addMatchCondition((tm, t, o) -> matchCondition.test(o)); + } + void addMatchCondition(ParameterMatcher matchCondition) { + if (matchConditions == null) { + matchConditions = new ArrayList<>(); + } + matchConditions.add(matchCondition); + } + + public List getSubstitutionRequests() { + return substitutionRequests; + } + + public boolean matches(TemplateMatcher templateMatcher, CtElement target, CtElement template) { + if (matchConditions == null) { + //there is no matching condition. Everything matches + return true; + } + //there is some matching condition. At least one must match + for (ParameterMatcher predicate : matchConditions) { + if (predicate.match(templateMatcher, template, target)) { + return true; + } + } + return false; + } + + public ValueConvertor getValueConvertor() { + return this.valueConvertor != null ? this.valueConvertor : pattern.getValueConvertor(); + } + + public void setValueConvertor(ValueConvertor valueConvertor) { + this.valueConvertor = valueConvertor; + } +} diff --git a/src/main/java/spoon/pattern/ParameterType.java b/src/main/java/spoon/pattern/ParameterType.java new file mode 100644 index 00000000000..2de65c6decf --- /dev/null +++ b/src/main/java/spoon/pattern/ParameterType.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.pattern; + +/** + */ +public enum ParameterType { + TYPE, + NAME, + EXPRESSION, + STATEMENT; +} diff --git a/src/main/java/spoon/pattern/Pattern.java b/src/main/java/spoon/pattern/Pattern.java new file mode 100644 index 00000000000..9cd97e1cd17 --- /dev/null +++ b/src/main/java/spoon/pattern/Pattern.java @@ -0,0 +1,319 @@ +/** + * 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.pattern; + +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import spoon.SpoonException; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.path.CtRole; +import spoon.support.visitor.equals.CloneHelper; + +/** + * {@link Pattern} knows the AST of the pattern model. The pattern model is not modified or used as result of substitution. We always clone the pattern model. + * It knows list of parts of pattern model, which are target for substitution. + * + * The substitution target can be: + * A) node replace parameters - it means whole AST node (subtree) is replaced by value of parameter + * The type of such value is defined by parent attribute which holds this node: + * Examples: CtTypeMember, CtStatement, CtExpression, CtParameter, ... + * A1) Single node replace parameter - there must be exactly one node (with arbitrary subtree) as parameter value + * Examples: + * CtCatch.parameter, CtReturn.expression, CtBinaryOperator.leftOperand, + * CtForEach.variable, + * CtLoop.body, + * CtField.type + * ... + * A2) Multiple nodes replace parameter - there must be 0, 1 or more nodes (with arbitrary subtrees) as parameter value + * Examples: + * CtType.interfaces, CtType.typeMembers + * note: There can be 0 or 1 parameter assigned to model node + * + * Definition of such CtElement based parameters: + * ------------------------------ + * - by `TemplateParameter.S()` - it works only for some node types. Does not work for CtCase, CtCatch, CtComment, CtAnnotation, CtEnumValue, ... + * - by pointing to such node(s) - it works for all nodes. How? During building of Pattern, the client's code has to somehow select the parameter nodes + * and add them into list of to be substituted nodes. Client may use + * - Filter ... here we can filter for `TemplateParameter.S()` + * - CtPath ... after it is fully implemented + * - Filtering by their name - legacy templates are using that approach together with Parameter annotation + * - manual navigation and collecting of substituted nodes + * + * B) node attribute replace - it means value of node attribute is replaced by value of parameter + * B1) Single value attribute - there must be exactly one value as parameter value + * Types are String, boolean, BinaryOperatorKind, UnaryOperatorKind, CommentType, primitive type of Literal.value + * B2) Unordered multiple value attribute - there must be exactly one value as parameter value + * There is only: CtModifiable.modifiers with type Enum ModifierKind + * + * note: There can be no parameter of type (A) assigned to node whose attributes are going to be replaced. + * There can be more attributes replaced for one node + * But there can be 0 or 1 parameter assigned to attribute of model node + * + * Definition of such Object based parameters: + * ------------------------------------------------------ + * by pointing to such node(s) + * + with specification of CtRole of that attribute + * + * C) Substring attribute replace - it means substring of string value is replaced + * Examples: CtNamedElement.simpleName, CtStatement.label, CtComment.comment + * + * note: There can be no parameter of type (A) assigned to node whose String attributes are going to be replaced. + * There can be 0, 1 or more parameter assigned to String of model node. Each must have different identifier. + * + * Definition of such parameters: + * ------------------------------ + * by pointing to such node(s) + * + with specification of CtRole of that attribute + * + with specification of to be replaced substring + * It can be done by searching in all String attributes of each node and searching for a variable marker. E.g. "$var_name$" + * + * Optionally there might be defined a variable value formatter, which assures that variable value is converted to expected string representation + * + * Why {@link Pattern} needs such high flexibility? + * Usecase: The Pattern instance might be created by comparing of two similar models (not templates, but part of normal code). + * All the differences anywhere would be considered as parameters of generated Pattern instance. + * Request: Such pattern instance must be printable and compilable, so client can use it for further matching and replacing by different pattern. + */ +public class Pattern { + private final CtElement patternModel; + private Map patternElementToSubstRequests = new IdentityHashMap<>(); + private ValueConvertor valueConvertor; + + Pattern(CtElement patternModel) { + this.patternModel = patternModel; + } + + /** + * @return a pattern model of this pattern + */ + public CtElement getModel() { + return patternModel; + } + + /** + * @param patternModelElement to be checked element of pattern model + * @return {@link SubstitutionRequest} linked to the `patternModelElement` or null if this element is not a target for the parameter substitution + */ + public AbstractNodeSubstitutionRequest getSubstitutionRequest(CtElement patternModelElement) { + return patternElementToSubstRequests.get(patternModelElement); + } + + /** + * generates a new AST made by clonning of `patternModel` and by substitution of parameters by values in `params` + * @param params + * @return + */ + @SuppressWarnings("unchecked") + public List substitute(Map params) { + return new ModelCloner(params).clone(Collections.singletonList((T) patternModel)); + } + + class ModelCloner extends CloneHelper { + Map params; + + ModelCloner(Map params) { + super(); + this.params = params; + } + + /** + * Handle substitution of single value element. + * Zero or one element can be result of substitution of `element` + */ + @Override + public T clone(T origin) { + List substitution = substituteOrClone(origin); + if (substitution.isEmpty()) { + //the pattern removes this element. Return null as new value + return null; + } + if (substitution.size() == 1) { + //there is one substituted value. Return it + return substitution.get(0); + } + throw new SpoonException("Cannot substitute value of single value attribute by multiple values"); + } + + /** + * Handle substitution of element in a collection. + * Zero, one or more elements can be result of substitution of `element` + */ + @Override + protected void addClone(Collection targetCollection, T origin) { + targetCollection.addAll(substituteOrClone(origin)); + } + + /** + * Handle substitution of element in a Map. + * Zero or one element can be result of substitution of `element` + */ + @Override + protected void addClone(Map targetMap, String key, T originValue) { + List substitution = substituteOrClone(originValue); + if (substitution.isEmpty()) { + //the pattern removes this element. Do not add it into target map + return; + } + if (substitution.size() == 1) { + //there is one substituted value. Add it + targetMap.put(key, substitution.get(0)); + return; + } + throw new SpoonException("Cannot substitute value of Map by multiple values"); + } + + /** + * Clones or substitutes `origin` element, depending on SubstitutionRequests for this element. + * @param origin to be cloned/substituted element. + * @return zero, one or more cloned/substituted elements + */ + protected List substituteOrClone(T origin) { + AbstractNodeSubstitutionRequest substRequest = patternElementToSubstRequests.get(origin); + if (substRequest == null) { + //there is no substitution request just clone it + return Collections.singletonList(originClone(origin)); + } + //there is substitution request, let it substitute + return substRequest.substitute(this, params); + } + + /** + * @param element to be cloned element + * @return a clone which is not substituted + */ + public T originClone(T element) { + return super.clone(element); + } + } + + /** + * @param element to be checked element + * calls consumer for each {@link SubstitutionRequest} of the `element` or never if there is no {@link SubstitutionRequest}. + */ +// public void forEachSubstitutionRequestsOfPatternElement(CtElement element, Consumer> consumer) { +// List> srs = getPatternElementToSubstitutionRequests().get(element); +// if (srs != null) { +// for (SubstitutionRequest substitutionRequest : srs) { +// consumer.accept(substitutionRequest); +// } +// } +// } + + /** + * Adds request to substitute `element`, by the value of this {@link Pattern} parameter {@link ParameterInfo} value + * @param element to be replaced element + */ + public NodeSubstitutionRequest addSubstitutionRequest(ParameterInfo parameter, CtElement element) { + AbstractNodeSubstitutionRequest existingSR = patternElementToSubstRequests.get(element); + if (existingSR != null) { + if (existingSR instanceof NodeSubstitutionRequest) { + if (((NodeSubstitutionRequest) existingSR).getParameterInfo() == parameter) { + //the substitution request of required type already exists + return (NodeSubstitutionRequest) existingSR; + } + throw new SpoonException("Cannot add replace request to node which already contains a substitution request"); + } + } + NodeSubstitutionRequest rr = new NodeSubstitutionRequest(parameter, element); + patternElementToSubstRequests.put(element, rr); + return rr; + } + + /** + * Adds request to substitute value of `attributeRole` of `element`, by the value of this {@link Pattern} parameter {@link ParameterInfo} value + * @param element whose attribute of {@link CtRole} `attributeRole` have to be replaced + */ + public void addSubstitutionRequest(ParameterInfo parameter, CtElement element, CtRole attributeRole) { + addAttributeSubstRequest(element, owner -> new AttributeSubstitutionRequest(owner, parameter, attributeRole)); + } + + /** + * Adds request to substitute substring of {@link String} value of `attributeRole` of `element`, by the value of this {@link Pattern} parameter {@link ParameterInfo} value + * @param element whose part of String attribute of {@link CtRole} `attributeRole` have to be replaced + */ + public void addSubstitutionRequest(ParameterInfo parameter, CtElement element, CtRole attributeRole, String subStringMarker) { + addAttributeSubstRequest(element, owner -> { + AbstractAttributeSubstitutionRequest attrReq = owner.getAttributeSubstititionRequest(attributeRole); + SubstringSubstitutionRequest ssr; + if (attrReq instanceof SubstringSubstitutionRequest) { + ssr = (SubstringSubstitutionRequest) attrReq; + } else { + if (attrReq != null) { + throw new SpoonException("Cannot add substring substitution request to already substituted attribute"); + } + ssr = new SubstringSubstitutionRequest(owner, attributeRole); + } + ssr.setReplaceMarker(parameter, subStringMarker); + return ssr; + }); + } + + private void addAttributeSubstRequest(CtElement element, Function attrSubstRequestCreator) { + AbstractNodeSubstitutionRequest sr = patternElementToSubstRequests.get(element); + NodeAttributesSubstitionRequest attrSubstReq; + if (sr != null) { + if (sr instanceof NodeSubstitutionRequest) { + //Cannot add attribute substitution request to node, which already contains a NodeSubstitutionRequest + //NodeSubstitutionRequest has higher priority + return; + } + attrSubstReq = (NodeAttributesSubstitionRequest) sr; + } else { + attrSubstReq = new NodeAttributesSubstitionRequest(element); + patternElementToSubstRequests.put(element, attrSubstReq); + } + attrSubstReq.addAttributeSubstitutionRequest(attrSubstRequestCreator.apply(attrSubstReq)); + } + + + static V getOrCreate(Map map, K key, Supplier valueCreator) { + V value = map.get(key); + if (value == null) { + value = valueCreator.get(); + map.put(key, value); + } + return value; + } + + /** + * @param element to be checked element + * @return true if element `element` is a template or a child of template + */ + public boolean iInModel(CtElement element) { + while (element != null) { + if (element == patternModel) { + return true; + } + element = element.isParentInitialized() ? element.getParent() : null; + } + return false; + } + + public ValueConvertor getValueConvertor() { + return valueConvertor; + } + + public void setValueConvertor(ValueConvertor valueConvertor) { + this.valueConvertor = valueConvertor; + } +} diff --git a/src/main/java/spoon/pattern/PatternBuilder.java b/src/main/java/spoon/pattern/PatternBuilder.java new file mode 100644 index 00000000000..c30172bafc3 --- /dev/null +++ b/src/main/java/spoon/pattern/PatternBuilder.java @@ -0,0 +1,708 @@ +/** + * 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.pattern; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import spoon.SpoonException; +import spoon.reflect.code.CtBlock; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.code.CtReturn; +import spoon.reflect.code.CtStatement; +import spoon.reflect.code.CtVariableAccess; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtExecutable; +import spoon.reflect.declaration.CtField; +import spoon.reflect.declaration.CtType; +import spoon.reflect.declaration.CtTypeMember; +import spoon.reflect.declaration.CtVariable; +import spoon.reflect.factory.Factory; +import spoon.reflect.meta.RoleHandler; +import spoon.reflect.meta.impl.RoleHandlerHelper; +import spoon.reflect.path.CtRole; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtTypeReference; +import spoon.reflect.reference.CtVariableReference; +import spoon.reflect.visitor.CtScanner; +import spoon.reflect.visitor.Filter; +import spoon.reflect.visitor.filter.AllTypeMembersFunction; +import spoon.reflect.visitor.filter.TypeFilter; +import spoon.reflect.visitor.filter.VariableReferenceFunction; +import spoon.template.Parameter; +import spoon.template.TemplateParameter; + +/** + * The builder which creates a {@link Pattern} + */ +public class PatternBuilder { + + /** + * Creates a template builder for a template of the AST of the `element` + * @param element the AST, which will be used as source of the template + * @return {@link PatternBuilder} + */ + public static PatternBuilder createPattern(CtElement element) { + //if element instance of CtType, then create TypePatternBuilder? + return new PatternBuilder(element); + } + + /** + * Creates a template builder for a template of the {@link CtType} + * Optional `modelSelector` callback can be used to define which part of `type` has to be used as pattern model. + * `modelSelector` is called with clone of `type`. `modelSelector` should remove all the parts of cloned `type` + * and keep only these parts which has to be part of pattern model. + * @param type the CtType whose AST will be used as source of the template + * @return {@link PatternBuilder} + */ + public static PatternBuilder createTypePattern(CtType type, Consumer modelSelector) { + CtType clonedType = type.clone(); + //I am not sure yet, whether template model will need to know it's context + //- e.g. to resolve variables (which are not part of template model) from references (in template model) +// if (type.isParentInitialized()) { +// clonedType.setParent(type.getParent()); +// } + TypeView tv = new TypeView(clonedType); + modelSelector.accept(tv); + return createPattern(tv.template); + } + + /** + * Creates a template builder for a template of the {@link CtTypeMember} + * @param type {@link CtType} which contains the {@link CtTypeMember} whose AST will be used as source of the template + * @param filter the {@link Filter} whose match defines to be used {@link CtTypeMember} + * @return {@link PatternBuilder} + * + * Note: can be replaced by PatternBuilder.createPattern(type.filterChildren(filter).first()) + */ + public static PatternBuilder createTypeMemberPattern(CtType type, Filter filter) { + CtTypeMember template = null; + for (CtTypeMember typeMember : new ArrayList<>(type.getTypeMembers())) { + if (filter.matches(typeMember)) { + if (template != null) { + throw new SpoonException("Ambiguous type member definition. There are " + template.getShortRepresentation() + " and " + typeMember.getShortRepresentation()); + } + template = typeMember; + } + } + if (template == null) { + throw new SpoonException("Type member definition not found in " + type.getShortRepresentation()); + } + return new PatternBuilder((CtTypeMember) template); + } + + /** + * Creates a template builder for a template of one or more statements + * @param type {@link CtType} which contains the {@link CtExecutable} whose body AST will be used as source of the template + * @param filter the {@link Filter} whose match defines to be used {@link CtExecutable} + * @return {@link PatternBuilder} + * + * CtPath expressions might be used instead to configure such pattern + */ + public static PatternBuilder createStatementsPattern(CtType type, Filter> filter) { + CtExecutable template = null; + for (CtTypeMember typeMember : new ArrayList<>(type.getTypeMembers())) { + if (typeMember instanceof CtExecutable) { + CtExecutable exec = (CtExecutable) typeMember; + if (filter.matches(exec)) { + if (template != null) { + throw new SpoonException("Ambiguous type member definition. There are " + template.getShortRepresentation() + " and " + exec.getShortRepresentation()); + } + template = exec; + } + } + } + if (template == null) { + throw new SpoonException("Type member definition not found in " + type.getShortRepresentation()); + } + return new PatternBuilder(template.getBody()); + } + + /** + * Creates a template builder for a template of expression + * @param type {@link CtType} which contains the {@link CtExecutable} whose body return expression AST be used as source of the template + * @param filter the {@link Filter} whose match defines to be used {@link CtExecutable} + * @return {@link PatternBuilder} + */ + public static PatternBuilder createExpressionPattern(CtType type, Filter> filter) { + CtExecutable template = null; + for (CtTypeMember typeMember : new ArrayList<>(type.getTypeMembers())) { + if (typeMember instanceof CtExecutable) { + CtExecutable exec = (CtExecutable) typeMember; + if (filter.matches(exec)) { + if (template != null) { + throw new SpoonException("Ambiguous type member definition. There are " + template.getShortRepresentation() + " and " + exec.getShortRepresentation()); + } + template = exec; + } + } + } + if (template == null) { + throw new SpoonException("Type member definition not found in " + type.getShortRepresentation()); + } + CtBlock body = template.getBody(); + if (body.getStatements().size() != 1) { + throw new SpoonException("The body of " + template.getSignature() + " must contain exactly one statement. But there is:\n" + body.toString()); + } + CtStatement firstStatement = body.getStatements().get(0); + if (firstStatement instanceof CtReturn) { + CtReturn retStat = (CtReturn) firstStatement; + return new PatternBuilder(retStat.getReturnedExpression()); + } else { + throw new SpoonException("The body of " + template.getSignature() + " must contain return statement. But there is:\n" + body.toString()); + } + } + + private Map parameters = new LinkedHashMap<>(); + private boolean built = false; + private Pattern pattern; + + PatternBuilder(CtElement template) { + pattern = new Pattern(template); + pattern.setValueConvertor(new ValueConvertorImpl(template.getFactory())); + } + + public Pattern build() { + return pattern; + } + + public PatternBuilder setValueConvertor(ValueConvertor valueConvertor) { + pattern.setValueConvertor(valueConvertor); + return this; + } + + public PatternBuilder configureAutomaticParameters() { + configureParameters(pb -> { + /* + * detect other parameters. + * contract: All variable references, which are declared outside of template are automatically considered as template parameters + */ + pattern.getModel().filterChildren(new TypeFilter<>(CtVariableReference.class)) + .forEach((CtVariableReference varRef) -> { + CtVariable var = varRef.getDeclaration(); + if (var == null || pattern.iInModel(var) == false) { + //the varRef has declaration out of the scope of the template. It must be a template parameter. + if (pattern.getSubstitutionRequest(varRef) == null) { + //there is no substitution request registered for this variable reference yet. create one + ParameterInfo pi = pb.parameter(varRef.getSimpleName()).getCurrentParameter(); + addSubstitutionRequest(pi, getSubstitutedNodeOfVariableReference(varRef)); + } + } + }); + }); + return this; + } + + /** + * @return a node, which has to be substituted instead of variable reference `varRef` + */ + private CtElement getSubstitutedNodeOfVariableReference(CtVariableReference varRef) { + /* + * the target of substitution is always the parent node of variableReference + * - the expression - CtVariableAccess + * which can be replaced by any other CtVariableAccess. + * For example CtFieldRead can be replaced by CtVariableRead or by CtLiteral + */ + CtVariableAccess varAccess = (CtVariableAccess) varRef.getParent(); + CtElement varAccessParent = varAccess.getParent(); + if (varAccessParent instanceof CtInvocation) { + CtInvocation invocation = (CtInvocation) varAccessParent; + CtExecutableReference executableRef = invocation.getExecutable(); + if (executableRef.getSimpleName().equals("S")) { + if (TemplateParameter.class.getName().equals(executableRef.getDeclaringType().getQualifiedName())) { + /* + * the invocation of TemplateParameter#S() has to be substituted + */ + return invocation; + } + } + } + return varAccess; + } + + public static class TypeView { + + private CtType template; + + public TypeView(CtType template) { + this.template = template; + } + + /** + * @param filter whose matches will be removed from the template + */ + public TypeView removeTypeMembers(Filter filter) { + for (CtTypeMember ctTypeMember : new ArrayList<>(template.getTypeMembers())) { + if (filter.matches(ctTypeMember)) { + ctTypeMember.delete(); + } + } + return this; + } + + /** + * @param filter whose matches will be kept in the template - all others will be removed + */ + public TypeView keepTypeMembers(Filter filter) { + for (CtTypeMember ctTypeMember : new ArrayList<>(template.getTypeMembers())) { + if (filter.matches(ctTypeMember) == false) { + ctTypeMember.delete(); + } + } + return this; + } + + /** + * removes super class from the template + */ + public TypeView removeSuperClass() { + template.setSuperclass(null); + return this; + } + + /** + * @param filter super interfaces which matches the filter will be removed + */ + public TypeView removeSuperInterfaces(Filter> filter) { + Set> superIfaces = new HashSet<>(template.getSuperInterfaces()); + boolean changed = false; + for (Iterator> iter = superIfaces.iterator(); iter.hasNext();) { + if (filter.matches(iter.next())) { + iter.remove(); + changed = true; + } + } + if (changed) { + template.setSuperInterfaces(superIfaces); + } + return this; + } + + /** + * @param filter super interfaces which matches the filter will be kept. Others will be removed + */ + public TypeView keepSuperInterfaces(Filter> filter) { + Set> superIfaces = new HashSet<>(template.getSuperInterfaces()); + boolean changed = false; + for (Iterator> iter = superIfaces.iterator(); iter.hasNext();) { + if (filter.matches(iter.next())) { + iter.remove(); + changed = true; + } + } + if (changed) { + template.setSuperInterfaces(superIfaces); + } + return this; + } + } + + public PatternBuilder configureParameters(Consumer parametersBuilder) { + ParametersBuilder pb = new ParametersBuilder(); + parametersBuilder.accept(pb); + return this; + } + + /** + * adds all standard Template parameters based on {@link TemplateParameter} and {@link Parameter} annotation + * @param templateType the CtType which contains template parameters + * @param templateParameters parameters, which will be used in substitution. It is needed here, + * because parameter value types influences which AST nodes will be the target of substitution + * @return this to support fluent API + */ + public PatternBuilder configureTemplateParameters(CtType templateType, Map templateParameters) { + @SuppressWarnings("rawtypes") + CtTypeReference templateParamRef = templateType.getFactory().Type().createReference(TemplateParameter.class); + CtTypeReference typeReferenceRef = templateType.getFactory().Type().createReference(CtTypeReference.class); + configureParameters(params -> { + templateType.map(new AllTypeMembersFunction()).forEach((CtTypeMember typeMember) -> { + Parameter param = typeMember.getAnnotation(Parameter.class); + if (param != null) { + if (typeMember instanceof CtField) { + CtField paramField = (CtField) typeMember; + /* + * We have found a CtField annotated by @Parameter. + * Use it as Pattern parameter + */ + String parameterName = typeMember.getSimpleName(); + String stringMarker = (param.value() != null && param.value().length() > 0) ? param.value() : parameterName; + //for the compatibility reasons with Parameters.getNamesToValues(), use the proxy name as parameter name + parameterName = stringMarker; + + CtTypeReference paramType = paramField.getType(); + if (paramType.isSubtypeOf(typeReferenceRef) || paramType.getQualifiedName().equals(Class.class.getName())) { + /* + * parameter with value type TypeReference or Class, identifies replacement of local type whose name is equal to parameter name + */ + params.parameter(parameterName).byLocalType(templateType, stringMarker); + } else if (paramType.getQualifiedName().equals(String.class.getName())) { +// CtType nestedType = getLocalTypeBySimpleName(templateType, stringMarker); +// if (nestedType != null) { +// //There is a local type with such name. Replace it +// params.parameter(parameterName).byType(nestedType.getReference()); +// } + } else if (paramType.isSubtypeOf(templateParamRef)) { + params.parameter(parameterName) + .byTemplateParameterReference(paramField); + } else { + //it is not a String. It is used to substitute CtLiteral of parameter value + params.parameter(parameterName) + //all occurrences of parameter name in pattern model are subject of substitution + .byVariableReference(paramField); + } + if (paramType.getQualifiedName().equals(Object.class.getName()) && templateParameters != null) { + //if the parameter type is Object, then detect the real parameter type from the parameter value + Object value = templateParameters.get(parameterName); + if (value instanceof CtLiteral || value instanceof CtTypeReference) { + /* + * the real parameter value is CtLiteral or CtTypeReference + * We should replace all method invocations whose name equals to stringMarker + * by that CtLiteral or qualified name of CtTypeReference + */ + ParameterInfo pi = params.parameter(parameterName).getCurrentParameter(); + pattern.getModel().filterChildren((CtInvocation inv) -> { + return inv.getExecutable().getSimpleName().equals(stringMarker); + }).forEach((CtInvocation inv) -> { + addSubstitutionRequest(pi, inv); + }); + } + } + + //any value can be converted to String. Substitute content of all string attributes + params.parameter(parameterName) + .bySubstring(stringMarker); + } else { + //TODO CtMethod was may be supported in old Template engine!!! + throw new SpoonException("Template Parameter annotation on " + typeMember.getClass().getName() + " is not supported"); + } + } else if (typeMember instanceof CtField && ((CtField) typeMember).getType().isSubtypeOf(templateParamRef)) { + CtField field = (CtField) typeMember; + String parameterName = typeMember.getSimpleName(); + params.parameter(parameterName) + .byTemplateParameterReference(field); + } + }); + //all references to templateType are parameter too + params.parameter(templateType.getSimpleName()).byType(templateType.getReference()); + }); + return this; + } + + private CtTypeReference getLocalTypeRefBySimpleName(CtType templateType, String typeSimpleName) { + CtType type = templateType.getNestedType(typeSimpleName); + if (type != null) { + return type.getReference(); + } + type = templateType.getPackage().getType(typeSimpleName); + if (type != null) { + return type.getReference(); + } + Set typeQNames = new HashSet<>(); + templateType + .filterChildren((CtTypeReference ref) -> typeSimpleName.equals(ref.getSimpleName())) + .forEach((CtTypeReference ref) -> typeQNames.add(ref.getQualifiedName())); + if (typeQNames.size() > 1) { + throw new SpoonException("The type parameter " + typeSimpleName + " is ambiguous. It matches multiple types: " + typeQNames); + } + if (typeQNames.size() == 1) { + return templateType.getFactory().Type().createReference(typeQNames.iterator().next()); + } + return null; + } + + public class ParametersBuilder { + ParameterInfo currentParameter; + + /** + * Creates a parameter with name `paramName` and assigns it into context, so next calls on builder will be applied to this parameter + * @param paramName to be build parameter name + * @return this {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder parameter(String paramName) { + currentParameter = parameters.get(paramName); + if (currentParameter == null) { + currentParameter = new ParameterInfo(PatternBuilder.this.pattern, paramName); + parameters.put(paramName, currentParameter); + } + return this; + } + + /** + * @return true if parameter `paramName` already contains at least one substitution request + */ + public boolean isSubstituted(String paramName) { + ParameterInfo pi = parameters.get(paramName); + if (pi != null) { + return pi.getSubstitutionRequests().size() > 0; + } + return false; + } + + protected ParameterInfo getCurrentParameter() { + if (currentParameter == null) { + throw new SpoonException("Parameter name must be defined first by call of #parameter(String) method."); + } + return currentParameter; + } + + /** + * `type` itself and all the references to the `type` are subject for substitution by current parameter + * @param type to be substituted Class + * @return {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder byType(Class type) { + return byType(type.getName()); + } + /** + * type identified by `typeQualifiedName` itself and all the references to that type are subject for substitution by current parameter + * @param typeQualifiedName a fully qualified name of to be substituted Class + * @return {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder byType(String typeQualifiedName) { + return byType(getFactory().Type().createReference(typeQualifiedName)); + } + /** + * type referred by {@link CtTypeReference} `type` and all the references to that type are subject for substitution by current parameter + * @param type a fully qualified name of to be substituted Class + * @return {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder byType(CtTypeReference type) { + ParameterInfo pi = getCurrentParameter(); + //substitute all references to that type + pattern.getModel().filterChildren((CtTypeReference typeRef) -> typeRef.equals(type)) + .forEach((CtTypeReference typeRef) -> { + addSubstitutionRequest(pi, typeRef); + }); + /** + * If Type itself is found part of model, then substitute it's simple name too + */ + String typeQName = type.getQualifiedName(); + CtType type2 = pattern.getModel() + .filterChildren((CtType t) -> t.getQualifiedName().equals(typeQName)) + .first(); + if (type2 != null) { + //Substitute name of template too + addSubstitutionRequest(pi, type2, CtRole.NAME); + } + return this; + } + + /** + * Searches for a type visible in scope `templateType`, whose simple name is equal to `localTypeSimpleName` + * @param searchScope the Type which is searched for local Type + * @param localTypeSimpleName the simple name of to be returned Type + * @return {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder byLocalType(CtType searchScope, String localTypeSimpleName) { + CtTypeReference nestedType = getLocalTypeRefBySimpleName(searchScope, localTypeSimpleName); + if (nestedType == null) { + throw new SpoonException("Template parameter " + localTypeSimpleName + " doesn't match to any local type"); + } + //There is a local type with such name. Replace it + byType(nestedType); + return this; + } + + /** + * variable read/write of `variable` + * @param variable a variable whose references will be substituted + * @return {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder byVariableReference(CtVariable variable) { + ParameterInfo pi = getCurrentParameter(); + pattern.getModel().map(new VariableReferenceFunction(variable)) + .forEach((CtVariableReference varRef) -> { + addSubstitutionRequest(pi, getSubstitutedNodeOfVariableReference(varRef)); + }); + return this; + } + + /** + * variable read/write of `variable` of type {@link TemplateParameter} + * @param variable a variable whose references will be substituted + * @return {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder byTemplateParameterReference(CtVariable variable) { + ParameterInfo pi = getCurrentParameter(); + pattern.getModel().map(new VariableReferenceFunction(variable)) + .forEach((CtVariableReference varRef) -> { + /* + * the target of substitution is always the invocation of TemplateParameter#S() + */ + CtVariableAccess varAccess = (CtVariableAccess) varRef.getParent(); + CtElement invocationOfS = varAccess.getParent(); + if (invocationOfS instanceof CtInvocation) { + CtInvocation invocation = (CtInvocation) invocationOfS; + if ("S".equals(invocation.getExecutable().getSimpleName())) { + addSubstitutionRequest(pi, invocation); + return; + } + } + throw new SpoonException("TemplateParameter reference is NOT used as target of invocation of TemplateParameter#S()"); + }); + return this; + } + + /** + * CodeElement element identified by `simpleName` + * @param simpleName the name of the element or reference + * @return {@link ParametersBuilder} to support fluent API + */ +// public ParametersBuilder codeElementBySimpleName(String simpleName) { +// ParameterInfo pi = getCurrentParameter(); +// pattern.getModel().filterChildren((CtNamedElement named) -> simpleName.equals(named.getSimpleName())) +// .forEach((CtNamedElement named) -> { +// if (named instanceof CtCodeElement) { +// addSubstitutionRequest(pi, named); +// } +// }); +// pattern.getModel().filterChildren((CtReference ref) -> simpleName.equals(ref.getSimpleName())) +// .forEach((CtReference ref) -> { +// if (ref instanceof CtTypeReference) { +// return; +// } +// CtCodeElement codeElement = ref.getParent(CtCodeElement.class); +// if (codeElement != null) { +// addSubstitutionRequest(pi, codeElement); +// } +// }); +// return this; +// } + + /** + * All spoon model string attributes whose value is equal to `stringMarker` + * are subject for substitution by current parameter + * @param stringMarker a string value which has to be substituted + * @return {@link ParametersBuilder} to support fluent API + */ +// public ParametersBuilder byString(String stringMarker) { +// ParameterInfo pi = getCurrentParameter(); +// new StringAttributeScanner() { +// @Override +// protected void visitStringAttribute(RoleHandler roleHandler, CtElement element, String value) { +// if (stringMarker.equals(value)) { +// if (roleHandler.getRole() == CtRole.NAME) { +// /* +// * We have found exact match of stringMarker with simpleName of element. +// * It has to substitute whole element, not only the name +// */ +// addSubstitutionRequest(pi, element); +// } else { +// addSubstitutionRequest(pi, element, roleHandler.getRole()); +// } +// } +// } +// }.scan(pattern.getModel()); +// return this; +// } + + /** + * All spoon model string attributes whose value contains whole string or a substring equal to `stringMarker` + * are subject for substitution by current parameter. Only the `stringMarker` substring of the string value is substituted! + * @param stringMarker a string value which has to be substituted + * @return {@link ParametersBuilder} to support fluent API + */ + public ParametersBuilder bySubstring(String stringMarker) { + ParameterInfo pi = getCurrentParameter(); + new StringAttributeScanner() { + @Override + protected void visitStringAttribute(RoleHandler roleHandler, CtElement element, String value) { + if (value != null && value.indexOf(stringMarker) >= 0) { + addSubstitutionRequest(pi, element, roleHandler.getRole(), stringMarker); + } + } + }.scan(pattern.getModel()); + return this; + } + + public ParametersBuilder matchCondition(Predicate matchCondition) { + getCurrentParameter().addMatchCondition(matchCondition); + return this; + } + } + + /** + * List of all Spoon model {@link RoleHandler}s, which provides access to attribute value of type String + */ + private static List stringAttributeRoleHandlers = new ArrayList<>(); + static { + RoleHandlerHelper.forEachRoleHandler(rh -> { + if (rh.getValueClass().isAssignableFrom(String.class)) { + //accept String and Object class + stringAttributeRoleHandlers.add(rh); + } + }); + } + + private abstract static class StringAttributeScanner extends CtScanner { + @Override + public void scan(CtElement element) { + visitStringAttribute(element); + super.scan(element); + } + private void visitStringAttribute(CtElement element) { + for (RoleHandler roleHandler : stringAttributeRoleHandlers) { + if (roleHandler.getTargetType().isInstance(element)) { + Object value = roleHandler.getValue(element); + if (value instanceof String) { + visitStringAttribute(roleHandler, element, (String) value); + } + //else it is a CtLiteral with non string value + } + } + } + protected abstract void visitStringAttribute(RoleHandler roleHandler, CtElement element, String value); + } + + protected Factory getFactory() { + return pattern.getModel().getFactory(); + } + + private void addSubstitutionRequest(ParameterInfo parameter, CtElement element) { + //if spoon creates an implicit parent (e.g. CtBlock) around the pattern parameter, then replace that implicit parent + CtElement lastImplicitParent = getLastImplicitParent(element); + pattern.addSubstitutionRequest(parameter, lastImplicitParent); + } + private void addSubstitutionRequest(ParameterInfo parameter, CtElement element, CtRole attributeRole) { + pattern.addSubstitutionRequest(parameter, element, attributeRole); + } + private void addSubstitutionRequest(ParameterInfo parameter, CtElement element, CtRole attributeRole, String subStringMarker) { + pattern.addSubstitutionRequest(parameter, element, attributeRole, subStringMarker); + } + + /** + * @return last implicit parent of element + */ + private CtElement getLastImplicitParent(CtElement element) { + while (element.isParentInitialized()) { + CtElement parent = element.getParent(); + if ((parent instanceof CtBlock) == false || parent.isImplicit() == false) { + break; + } + element = parent; + } + return element; + } +} diff --git a/src/main/java/spoon/pattern/SubstitutionRequest.java b/src/main/java/spoon/pattern/SubstitutionRequest.java new file mode 100644 index 00000000000..923ca77f0e6 --- /dev/null +++ b/src/main/java/spoon/pattern/SubstitutionRequest.java @@ -0,0 +1,36 @@ +/** + * 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.pattern; + +import spoon.reflect.declaration.CtElement; +import spoon.reflect.path.CtRole; + +/** + * Represents the request for substitution of one value in Pattern model + */ +public interface SubstitutionRequest { + + /** + * @return {@link CtRole} whose value will be substituted + */ + CtRole getRoleOfSubstitutedValue(); + + /** + * @return element whose attribute or child node will be substituted + */ + CtElement getParentNode(); +} diff --git a/src/main/java/spoon/pattern/SubstringSubstitutionRequest.java b/src/main/java/spoon/pattern/SubstringSubstitutionRequest.java new file mode 100644 index 00000000000..d03ec7de715 --- /dev/null +++ b/src/main/java/spoon/pattern/SubstringSubstitutionRequest.java @@ -0,0 +1,177 @@ +/** + * 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.pattern; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import spoon.SpoonException; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.path.CtRole; +import spoon.template.TemplateMatcher; + +/** + * Represents the request for substitution of substring of string value of attribute of an AST node + * Examples: Substrings in CtNamedElement.simpleName, CtStatement.label, CtComment.comment + */ +public class SubstringSubstitutionRequest extends AbstractAttributeSubstitutionRequest { + + /* + * Use LinkedHashMap to assure defined replacement order + */ + private final Map tobeReplacedSubstrings = new LinkedHashMap<>(); + private ParameterInfo[] params; + private Pattern pattern; + + SubstringSubstitutionRequest(NodeAttributesSubstitionRequest owner, CtRole roleOfSubstitutedValue) { + super(owner, roleOfSubstitutedValue); + } + + /** + * @return The string whose occurrence in target string will be replaced by parameter value + */ + public String getReplaceMarker(ParameterInfo param) { + return tobeReplacedSubstrings.get(param); + } + /** + * Defines that this Substitution request will replace all occurrences of `replaceMarker` in target string by value of `param` + * @param param the declaration of to be replaced parameter + * @param replaceMarker the substring whose occurrences will be substituted + */ + public void setReplaceMarker(ParameterInfo param, String replaceMarker) { + tobeReplacedSubstrings.put(param, replaceMarker); + } + + @Override + public void substitute(T clonedElement, Map params) { + String stringValue = clonedElement.getValueByRole(getRoleOfSubstitutedValue()); + for (Map.Entry requests : tobeReplacedSubstrings.entrySet()) { + ParameterInfo param = requests.getKey(); + String replaceMarker = requests.getValue(); + ConversionContext context = new ConversionContext(this, param, params); + context.setRequiredClass(String.class); + //we have to force String here, because some roleHandlers have type Object + List convertedValues = param.getValueConvertor().getValueAs(context); + if (convertedValues.size() > 1) { + throw new SpoonException("Cannot substitute substring by multiple values: " + convertedValues); + } + String substrValue = convertedValues.isEmpty() ? "" : convertedValues.get(0); + stringValue = substituteSubstring(stringValue, replaceMarker, substrValue); + } + clonedElement.setValueByRole(getRoleOfSubstitutedValue(), stringValue); + } + + /** + * Replaces all occurrences of `tobeReplacedSubstring` in `str` by `substrValue` + * @param str to be modified string + * @param substrValue new value + * @return replaced string + */ + private String substituteSubstring(String str, String tobeReplacedSubstring, String substrValue) { + return str.replaceAll(escapeRegExp(tobeReplacedSubstring), substrValue); + } + + @Override + public boolean matches(TemplateMatcher matcher, Object target, Object template) { + String value = (String) target; + java.util.regex.Pattern re = getMatchingPattern(); + Matcher m = re.matcher(value); + if (m.matches() == false) { + return false; + } + ParameterInfo[] params = getMatchingParameterInfos(); + for (int i = 0; i < params.length; i++) { + String paramValue = m.group(i + 1); + if (matcher.addMatch(params[i], paramValue) == false) { + //two occurrences of the same parameter are matching on different value + //whole string doesn't matches + return false; + } + } + return true; + } + + private ParameterInfo[] getMatchingParameterInfos() { + getMatchingPattern(); + return params; + } + + private static class Region { + ParameterInfo param; + int from; + int to; + + Region(ParameterInfo param, int from, int to) { + super(); + this.param = param; + this.from = from; + this.to = to; + } + } + + private synchronized Pattern getMatchingPattern() { + if (pattern == null) { + String patternValue = getSubstitutedNode().getValueByRole(getRoleOfSubstitutedValue()); + List regions = new ArrayList<>(); + List paramsByRegions = new ArrayList<>(); + for (Map.Entry markers : tobeReplacedSubstrings.entrySet()) { + addRegionsOf(regions, patternValue, markers.getKey(), markers.getValue()); + } + regions.sort((a, b) -> a.from - b.from); + StringBuilder re = new StringBuilder(); + int start = 0; + for (Region region : regions) { + if (region.from > start) { + re.append(escapeRegExp(patternValue.substring(start, region.from))); + } else if (start > 0) { + throw new SpoonException("Cannot detect string parts if parameter separators are missing in pattern value: " + patternValue); + } + re.append("(") //start RE matching group + .append(".*?") //match any character, but not greedy + .append(")"); //end of RE matching group + paramsByRegions.add(region.param); + start = region.to; + } + if (start < patternValue.length()) { + re.append(escapeRegExp(patternValue.substring(start))); + } + pattern = Pattern.compile(re.toString()); + params = paramsByRegions.toArray(new ParameterInfo[paramsByRegions.size()]); + } + return pattern; + } + + private void addRegionsOf(List regions, String patternValue, ParameterInfo param, String marker) { + int start = 0; + while (start < patternValue.length()) { + start = patternValue.indexOf(marker, start); + if (start < 0) { + return; + } + regions.add(new Region(param, start, start + marker.length())); + start += marker.length(); + } + } + + private String escapeRegExp(String str) { + return "\\Q" + str + "\\E"; + } +} diff --git a/src/main/java/spoon/pattern/TemplateBuilder.java b/src/main/java/spoon/pattern/TemplateBuilder.java new file mode 100644 index 00000000000..a27f9936c0c --- /dev/null +++ b/src/main/java/spoon/pattern/TemplateBuilder.java @@ -0,0 +1,133 @@ +/** + * 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.pattern; + + +import java.util.List; +import java.util.Map; + +import spoon.SpoonException; +import spoon.reflect.declaration.CtClass; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtField; +import spoon.reflect.declaration.CtType; +import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtTypeReference; +import spoon.support.template.Parameters; +import spoon.template.Local; +import spoon.template.Parameter; +import spoon.template.Substitution; +import spoon.template.Template; +import spoon.template.TemplateParameter; + +/** + * The builder which creates a {@link Pattern} from the {@link Template} + */ +public class TemplateBuilder { + + /** + * Creates a {@link Pattern} from {@link Template} + * @param templateRoot the root element of {@link Template} model + * @param template a instance of the {@link Template}. It is needed here, + * because parameter value types influences which AST nodes will be the target of substitution + * @return {@link TemplateBuilder} + */ + public static TemplateBuilder createPattern(CtElement templateRoot, Template template) { + CtClass> templateType = Substitution.getTemplateCtClass(templateRoot.getFactory(), template); + return createPattern(templateRoot, templateType, template); + } + public static TemplateBuilder createPattern(CtElement templateRoot, CtClass templateType, Template template) { + Factory f = templateRoot.getFactory(); + + if (template != null && templateType.getQualifiedName().equals(template.getClass().getName()) == false) { + throw new SpoonException("Unexpected template instance " + template.getClass().getName() + ". Expects " + templateType.getQualifiedName()); + } + + PatternBuilder pb; + + @SuppressWarnings("rawtypes") + CtTypeReference templateParamRef = f.Type().createReference(TemplateParameter.class); + if (templateType == templateRoot) { + //templateRoot is a class which extends from Template. We have to remove all Templating stuff from the patter model + pb = PatternBuilder.createTypePattern(templateType, tv -> { + tv.keepTypeMembers(typeMember -> { + if (typeMember.getAnnotation(Parameter.class) != null) { + //remove all type members annotated with @Parameter + return false; + } + if (typeMember.getAnnotation(Local.class) != null) { + //remove all type members annotated with @Local + return false; + } + //remove all Fields of type TemplateParameter + if (typeMember instanceof CtField && ((CtField) typeMember).getType().isSubtypeOf(templateParamRef)) { + return false; + } + //all other type members have to be part of the pattern model + return true; + }); + //remove `... extends Template`, which doesn't have to be part of pattern model + tv.removeSuperClass(); + }); + } else { + pb = PatternBuilder.createPattern(templateRoot); + } + Map templateParameters = template == null ? null : Parameters.getTemplateParametersAsMap(f, null, template); + pb.configureTemplateParameters(templateType, templateParameters); + return new TemplateBuilder(templateType, pb, template); + } + + private Template template; + private PatternBuilder patternBuilder; + private CtClass templateType; + + private TemplateBuilder(CtClass templateType, PatternBuilder patternBuilder, Template template) { + this.template = template; + this.patternBuilder = patternBuilder; + this.templateType = templateType; + } + + public Pattern build() { + return patternBuilder.build(); + } + + /** + * @param template the instance of the Template of this TemplateBuilder + * @return Map of template parameters from `template` + */ + public Map getTemplateParameters() { + return getTemplateParameters(null); + } + /** + * @param template the instance of the Template of this TemplateBuilder + * @param targetType the type which will receive the model generated using returned parameters + * @return Map of template parameters from `template` + */ + public Map getTemplateParameters(CtType targetType) { + Factory f = templateType.getFactory(); + return Parameters.getTemplateParametersAsMap(f, targetType, template); + } + + /** + * generates a new AST made by cloning of `patternModel` and by substitution of parameters by their values + * @param targetType the CtType, which will receive the result of substitution + * @return + */ + public List substitute(CtType targetType) { + return build().substitute(getTemplateParameters(targetType)); + } +} diff --git a/src/main/java/spoon/pattern/ValueConvertor.java b/src/main/java/spoon/pattern/ValueConvertor.java new file mode 100644 index 00000000000..cca3518eb37 --- /dev/null +++ b/src/main/java/spoon/pattern/ValueConvertor.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.pattern; + +import java.util.List; + +/** + * Converts the individual parameter values to required type + */ +public interface ValueConvertor { + List getValueAs(ConversionContext conversionContext); +} diff --git a/src/main/java/spoon/pattern/ValueConvertorImpl.java b/src/main/java/spoon/pattern/ValueConvertorImpl.java new file mode 100644 index 00000000000..4298c9c63a3 --- /dev/null +++ b/src/main/java/spoon/pattern/ValueConvertorImpl.java @@ -0,0 +1,177 @@ +/** + * 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.pattern; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import spoon.SpoonException; +import spoon.reflect.code.CtCodeElement; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.code.CtStatementList; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtExecutable; +import spoon.reflect.declaration.CtNamedElement; +import spoon.reflect.declaration.CtType; +import spoon.reflect.factory.Factory; +import spoon.reflect.meta.ContainerKind; +import spoon.reflect.reference.CtExecutableReference; +import spoon.reflect.reference.CtReference; +import spoon.reflect.reference.CtTypeReference; +import spoon.support.visitor.SignaturePrinter; + +/** + * Converts the individual parameter values to required type + */ +public class ValueConvertorImpl implements ValueConvertor { + + private final Factory factory; + + public ValueConvertorImpl(Factory factory) { + this.factory = factory; + } + + @Override + public List getValueAs(ConversionContext conversionContext) { + List result = new ArrayList<>(); + forEachItem(conversionContext.getValue(), singleValue -> { + if (singleValue != null) { + T convertedValue = (T) getSingleValueAs(singleValue, conversionContext); + if (convertedValue != null) { + result.add(convertedValue); + } + } + }); + if (conversionContext.getRoleHandler().getContainerKind() == ContainerKind.SINGLE && result.size() > 1) { + throw new SpoonException("Cannot convert multiple values into single value role " + conversionContext.getRoleHandler()); + } + + return result; + } + + @SuppressWarnings("unchecked") + protected Object getSingleValueAs(Object value, ConversionContext conversionContext) { + Class valueClass = conversionContext.getRequiredClass(); + if (valueClass.isInstance(value)) { + return cloneIfNeeded(value); + } + if (CtCodeElement.class.isAssignableFrom(valueClass)) { + if (value instanceof CtTypeReference) { + //convert type reference into code element as class access + CtTypeReference tr = (CtTypeReference) value; + return factory.Code().createClassAccess(tr); + } + if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Character) { + //convert String to code element as Literal + return factory.Code().createLiteral(value); + } + } + if (valueClass.equals(String.class)) { + if (value instanceof CtNamedElement) { + return ((CtNamedElement) value).getSimpleName(); + } else if (value instanceof CtReference) { + return ((CtReference) value).getSimpleName(); + } else if (value instanceof Class) { + return ((Class) value).getSimpleName(); + } else if (value instanceof CtInvocation) { + return getShortSignatureForJavadoc(((CtInvocation) value).getExecutable()); + } else if (value instanceof CtExecutableReference) { + return getShortSignatureForJavadoc((CtExecutableReference) value); + } else if (value instanceof CtExecutable) { + return getShortSignatureForJavadoc(((CtExecutable) value).getReference()); + } else if (value instanceof CtLiteral) { + Object val = ((CtLiteral) value).getValue(); + return val == null ? null : val.toString(); + } + throw new SpoonException("Parameter value has unexpected class: " + value.getClass().getName() + ", whose conversion to String is not supported"); + } + if (CtTypeReference.class.isAssignableFrom(valueClass)) { + if (value == null) { + throw new SpoonException("The null value is not valid substitution for CtTypeReference"); + } + if (value instanceof Class) { + return factory.Type().createReference((Class) value); + } else if (value instanceof CtTypeReference) { + return ((CtTypeReference) value).clone(); + } else if (value instanceof CtType) { + return ((CtType) value).getReference(); + } else if (value instanceof String) { + return factory.Type().createReference((String) value); + } else { + throw new RuntimeException("unsupported reference substitution"); + } + } + + throw new SpoonException("Parameter value class: " + value.getClass().getName() + " cannot be converted to class is: " + valueClass.getName() + " in role " + + conversionContext.getRoleHandler() + " on " + conversionContext.getParent().getPosition()); + } + + /* + * return the typical Javadoc style link Foo#method(). The class name is not fully qualified. + */ + private static String getShortSignatureForJavadoc(CtExecutableReference ref) { + SignaturePrinter sp = new SignaturePrinter(); + sp.writeNameAndParameters(ref); + return ref.getDeclaringType().getSimpleName() + CtExecutable.EXECUTABLE_SEPARATOR + sp.getSignature(); + } + + @SuppressWarnings("unchecked") + protected T cloneIfNeeded(T value) { + if (value instanceof CtElement) { + return (T) ((CtElement) value).clone(); + } + return value; + } + + /** + * calls consumer.accept(Object) once for each item of the `multipleValues` collection or array. + * If it is not a collection or array then it calls consumer.accept(Object) once with `multipleValues` + * If `multipleValues` is null then consumer.accept(Object) is not called + * @param multipleValues to be iterated potential collection of items + * @param consumer the receiver of items + */ + @SuppressWarnings("unchecked") + private void forEachItem(Object multipleValues, Consumer consumer) { + if (multipleValues == null) { + return; + } + if (multipleValues instanceof CtStatementList) { + //CtStatementList extends Iterable, but we want to handle it as one node. + consumer.accept(multipleValues); + return; + } + if (multipleValues instanceof Iterable) { + for (Object item : (Iterable) multipleValues) { + consumer.accept(item); + } + return; + } + if (multipleValues instanceof Object[]) { + for (Object item : (Object[]) multipleValues) { + consumer.accept(item); + } + return; + } + consumer.accept(multipleValues); + } + + public Factory getFactory() { + return factory; + } +} diff --git a/src/main/java/spoon/pattern/concept.md b/src/main/java/spoon/pattern/concept.md new file mode 100644 index 00000000000..8a1dfd3fa43 --- /dev/null +++ b/src/main/java/spoon/pattern/concept.md @@ -0,0 +1,101 @@ +Template definition +1) compilable easy understandable Template +2) The good names of template parameters - well assigned to AST nodes +3) even the attributes of AST nodes might be a parameters (e.g. modifier of class) + +Notes: +- it doesn't matter what is the current AST node at place of parameter. It will be replaced by parameter value converted to instance of expected type +- we have to define which Types, methodNames are variables and which already have required value +- we have to define which statements, expressions are optional/mandatory + e.g. by + if (optional1) { + ...some optional statements... + } + +Generation of code from Template +1) filling template parameters by values +2) cloning template AST +3) substituting cloned AST parameter nodes by values + +Types of template parameters +A) AST node of type CtStatement, CtExpression (e.g. CtVariableAccess, ...) +B) replacing of CtTypeReference by another CtTypeReference +C) replacing of whole or part of simpleName or Reference.name by String value +D) replacing of any attribute of AST node by value of appropriate type + +Searching for code, which matches template +1) definition of filters on searched nodes +2) matching with AST +3) creating of Map/Structure of parameter to matching value from AST + + +{@link Pattern} knows the AST of the pattern model. +It knows list of parts of pattern model, which are target for substitution. + +The substitution target can be: +A) node replace parameters - it means whole AST node (subtree) is replaced by value of parameter + The type of such value is defined by parent attribute which holds this node: + Examples: CtTypeMember, CtStatement, CtExpression, CtParameter, ... + A1) Single node replace parameter - there must be exactly one node (with arbitrary subtree) as parameter value + Examples: + CtCatch.parameter, CtReturn.expression, CtBinaryOperator.leftOperand, + CtForEach.variable, + CtLoop.body, + CtField.type + ... + A2) Multiple nodes replace parameter - there must be 0, 1 or more nodes (with arbitrary subtrees) as parameter value + Examples: + CtType.interfaces, CtType.typeMembers + note: There can be 0 or 1 parameter assigned to model node + +Definition of such CtElement based parameters: +------------------------------ +- by `TemplateParameter.S()` - it works only for some node types. Does not work for CtCase, CtCatch, CtComment, CtAnnotation, CtEnumValue, ... +- by pointing to such node(s) - it works for all nodes. How? During building of Pattern, the client's code has to somehow select the parameter nodes + and add them into list of to be substituted nodes. Client may use + - Filter ... here we can filter for `TemplateParameter.S()` + - CtPath ... after it is fully implemented + - Filtering by their name - legacy templates are using that approach together with Parameter annotation + - manual navigation and collecting of substituted nodes + +B) node attribute replace - it means value of node attribute is replaced by value of parameter + B1) Single value attribute - there must be exactly one value as parameter value + Types are String, boolean, BinaryOperatorKind, UnaryOperatorKind, CommentType, primitive type of Literal.value + B2) Unordered multiple value attribute - there must be exactly one value as parameter value + There is only: CtModifiable.modifiers with type Enum ModifierKind + + note: There can be no parameter of type (A) assigned to node whose attributes are going to be replaced. + There can be more attributes replaced for one node + But there can be 0 or 1 parameter assigned to attribute of model node + +Definition of such Object based parameters: +------------------------------------------------------ +by pointing to such node(s) + + with specification of CtRole of that attribute + +C) Substring attribute replace - it means substring of string value is replaced + Examples: CtNamedElement.simpleName, CtStatement.label, CtComment.comment + + note: There can be no parameter of type (A) assigned to node whose String attributes are going to be replaced. + There can be 0, 1 or more parameter assigned to String of model node. Each must have different identifier. + +Definition of such parameters: +------------------------------ +by pointing to such node(s) + + with specification of CtRole of that attribute + + with specification of to be replaced substring +It can be done by searching in all String attributes of each node and searching for a variable marker. E.g. "$var_name$" + +Optionally there might be defined a variable value formatter, which assures that variable value is converted to expected string representation + +Why {@link Pattern} needs such high flexibility? +Usecase: The Pattern instance might be created by comparing of two similar models (not templates, but part of normal code). +All the differences anywhere would be considered as parameters of generated Pattern instance. +Request: Such pattern instance must be printable and compilable, so client can use it for further matching and replacing by different pattern. + + +Why ParameterInfo type? +---------------------- +Can early check whether parameter values can be accepted by Pattern +Needs a validation by SubstitutionRequests of ParameterInfo +Can act as a filter of TemplateMatcher parameter value \ No newline at end of file diff --git a/src/main/java/spoon/reflect/meta/impl/AbstractRoleHandler.java b/src/main/java/spoon/reflect/meta/impl/AbstractRoleHandler.java index 29bc597c9d1..aa4c63897dc 100644 --- a/src/main/java/spoon/reflect/meta/impl/AbstractRoleHandler.java +++ b/src/main/java/spoon/reflect/meta/impl/AbstractRoleHandler.java @@ -111,6 +111,11 @@ public Map asMap(W element) { throw new SpoonException("The value of CtRole." + getRole().name() + " cannot be adapted to Map for " + element.getClass().getSimpleName()); }; + @Override + public String toString() { + return getTargetType().getName() + "#" + getRole().getCamelCaseName(); + } + // protected abstract Iterator iterator(T element); // protected abstract int size(T element); // protected abstract V get(T element, int index); @@ -134,8 +139,55 @@ public java.util.Collection asCollection(W element) { return asList(element); }; - public java.util.List asList(W element) { - return Collections.singletonList(getValue(element)); + public java.util.List asList(W e) { +// return Collections.singletonList(getValue(element)); + return new AbstractList() { + T element = castTarget(e); + + @Override + public int size() { + return 1; + } + + @SuppressWarnings("unchecked") + @Override + public X get(int index) { + return (X) SingleHandler.this.getValue(element); + } + + @Override + public X set(int index, X value) { + if (index != 0) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: 1"); + } + X oldValue = get(0); + SingleHandler.this.setValue(element, value); + return (X) oldValue; + } + @Override + public boolean add(X value) { + X oldValue = get(0); + if (oldValue != null) { + //single value cannot have more then one value + throw new SpoonException("Single value attribute cannot have more then one value"); + } + SingleHandler.this.setValue(element, value); + return true; + } + + @Override + public boolean remove(Object value) { + if (value == null) { + return false; + } + X oldValue = get(0); + if (value == oldValue) { + SingleHandler.this.setValue(element, null); + return true; + } + return false; + } + }; }; public java.util.Set asSet(W element) { @@ -188,6 +240,11 @@ public int size() { public X get(int index) { return (X) ListHandler.this.get(element, index); } + + @Override + public X set(int index, X value) { + return (X) ListHandler.this.set(element, index, castItemValue(value)); + } @Override public boolean add(X value) { return ListHandler.this.add(element, castItemValue(value)); @@ -220,6 +277,13 @@ protected V get(T element, int index) { return this.>getValue(element).get(index); } + protected V set(T element, int index, V value) { + List values = new ArrayList<>(this.>getValue(element)); + V ret = values.set(index, value); + setValue(element, values); + return ret; + } + protected int size(T element) { return this.>getValue(element).size(); } diff --git a/src/main/java/spoon/reflect/meta/impl/RoleHandlerHelper.java b/src/main/java/spoon/reflect/meta/impl/RoleHandlerHelper.java index d34e164b317..f2222197afc 100644 --- a/src/main/java/spoon/reflect/meta/impl/RoleHandlerHelper.java +++ b/src/main/java/spoon/reflect/meta/impl/RoleHandlerHelper.java @@ -17,7 +17,11 @@ package spoon.reflect.meta.impl; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; import spoon.SpoonException; import spoon.reflect.declaration.CtElement; @@ -32,6 +36,8 @@ public class RoleHandlerHelper { private RoleHandlerHelper() { } + private static Map, List> roleHandlersByClass = new HashMap<>(); + @SuppressWarnings("unchecked") private static final List[] roleHandlers = new List[CtRole.values().length]; static { @@ -56,6 +62,7 @@ public static RoleHandler getRoleHandler(Class targetClass, } return rh; } + /** * @param targetClass the class of the to be manipulated node * @param role defines the to be manipulated attribute @@ -71,4 +78,34 @@ public static RoleHandler getOptionalRoleHandler(Class targ } return null; } + + /** + * @param targetClass a Close whose handlers we are looking for + * @return all RoleHandlers available for the `targetClass` + */ + public static List getRoleHandlers(Class targetClass) { + List handlers = roleHandlersByClass.get(targetClass); + if (handlers == null) { + List modifiableHandlers = new ArrayList<>(); + forEachRoleHandler(roleHandler -> { + if (roleHandler.getTargetType().isAssignableFrom(targetClass)) { + modifiableHandlers.add(roleHandler); + } + }); + handlers = Collections.unmodifiableList(modifiableHandlers); + roleHandlersByClass.put(targetClass, handlers); + } + return handlers; + } + + /** + * @param consumer is called for each RoleHandler of SpoonModel + */ + public static void forEachRoleHandler(Consumer consumer) { + for (List list : roleHandlers) { + for (RoleHandler roleHandler : list) { + consumer.accept(roleHandler); + } + } + } } diff --git a/src/main/java/spoon/reflect/visitor/Filter.java b/src/main/java/spoon/reflect/visitor/Filter.java index 1b3142e1bdc..b1aef0ed2d4 100644 --- a/src/main/java/spoon/reflect/visitor/Filter.java +++ b/src/main/java/spoon/reflect/visitor/Filter.java @@ -16,6 +16,8 @@ */ package spoon.reflect.visitor; +import java.util.function.Predicate; + import spoon.reflect.declaration.CtElement; /** @@ -25,11 +27,15 @@ * the type of the filtered elements (an element belonging to the * filtered element must be assignable from T). */ -public interface Filter { +public interface Filter extends Predicate { /** * Tells if the given element matches. * @param element - the element to be checked for a match. Parameter element is never null if {@link Query} is used. */ boolean matches(T element); + @Override + default boolean test(T element) { + return matches(element); + } } diff --git a/src/main/java/spoon/support/template/Parameters.java b/src/main/java/spoon/support/template/Parameters.java index 02bc148c7ec..039acbcfb6d 100644 --- a/src/main/java/spoon/support/template/Parameters.java +++ b/src/main/java/spoon/support/template/Parameters.java @@ -269,7 +269,7 @@ public static boolean isParameterSource(CtFieldReference ref) { //the reference to this is not template parameter return false; } - if (TemplateParameter.class.getName().equals(ref.getType().getQualifiedName())) { + if (ref.getType().isSubtypeOf(getTemplateParameterType(ref.getFactory()))) { //the type of template field is TemplateParameter. return true; } diff --git a/src/main/java/spoon/support/template/SubstitutionVisitor.java b/src/main/java/spoon/support/template/SubstitutionVisitor.java index 20924cff0ce..5e2b832d818 100644 --- a/src/main/java/spoon/support/template/SubstitutionVisitor.java +++ b/src/main/java/spoon/support/template/SubstitutionVisitor.java @@ -75,6 +75,7 @@ class DoNotFurtherTemplateThisElement extends SpoonException { /** * This visitor implements the substitution engine of Spoon templates. */ +@Deprecated public class SubstitutionVisitor extends CtScanner { private static final Object NULL_VALUE = new Object(); diff --git a/src/main/java/spoon/support/visitor/equals/CloneHelper.java b/src/main/java/spoon/support/visitor/equals/CloneHelper.java index ec3df4b4b9c..f2ff4d6c753 100644 --- a/src/main/java/spoon/support/visitor/equals/CloneHelper.java +++ b/src/main/java/spoon/support/visitor/equals/CloneHelper.java @@ -55,7 +55,7 @@ public Collection clone(Collection elements) { } Collection others = new ArrayList<>(); for (T element : elements) { - others.add(clone(element)); + addClone(others, element); } return others; } @@ -69,7 +69,7 @@ public List clone(List elements) { } List others = new ArrayList<>(); for (T element : elements) { - others.add(clone(element)); + addClone(others, element); } return others; } @@ -100,7 +100,7 @@ public Set clone(Set elements) { Set others = createRightSet(elements); for (T element : elements) { - others.add(clone(element)); + addClone(others, element); } return others; } @@ -111,8 +111,27 @@ public Map clone(Map elements) { } Map others = new HashMap<>(); for (Map.Entry tEntry : elements.entrySet()) { - others.put(tEntry.getKey(), clone(tEntry.getValue())); + addClone(others, tEntry.getKey(), tEntry.getValue()); } return others; } + + /** + * clones a element and adds it's clone as value into targetCollection + * @param targetCollection - the collection which will receive a clone of element + * @param element to be cloned element + */ + protected void addClone(Collection targetCollection, T element) { + targetCollection.add(clone(element)); + } + + /** + * clones a value and adds it's clone as value into targetMap under key + * @param targetMap - the Map which will receive a clone of value + * @param key the target key, which has to be used to add cloned value into targetMap + * @param value to be cloned element + */ + protected void addClone(Map targetMap, String key, T value) { + targetMap.put(key, clone(value)); + } } diff --git a/src/main/java/spoon/template/AbstractTemplate.java b/src/main/java/spoon/template/AbstractTemplate.java index b0363ea1f59..ee7a9e2af72 100644 --- a/src/main/java/spoon/template/AbstractTemplate.java +++ b/src/main/java/spoon/template/AbstractTemplate.java @@ -22,7 +22,6 @@ import spoon.reflect.declaration.CtElement; import spoon.reflect.factory.Factory; import spoon.support.template.Parameters; -import spoon.support.template.SubstitutionVisitor; /** * handles the well-formedness and helper methods of templates @@ -61,14 +60,14 @@ public Factory getFactory() { } /** - * @return true if the template engine ({@link SubstitutionVisitor}) adds Generated by ... comments into generated code + * @return true if the template engine adds Generated by ... comments into generated code */ public boolean isAddGeneratedBy() { return addGeneratedBy; } /** - * @param addGeneratedBy if true the template engine ({@link SubstitutionVisitor}) will add Generated by ... comments into generated code + * @param addGeneratedBy if true the template engine will add Generated by ... comments into generated code */ public AbstractTemplate addGeneratedBy(boolean addGeneratedBy) { this.addGeneratedBy = addGeneratedBy; diff --git a/src/main/java/spoon/template/BlockTemplate.java b/src/main/java/spoon/template/BlockTemplate.java index e50202f760c..44772827757 100644 --- a/src/main/java/spoon/template/BlockTemplate.java +++ b/src/main/java/spoon/template/BlockTemplate.java @@ -16,6 +16,10 @@ */ package spoon.template; +import java.util.List; + +import spoon.SpoonException; +import spoon.pattern.TemplateBuilder; import spoon.reflect.code.CtBlock; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtType; @@ -48,7 +52,11 @@ public BlockTemplate() { public CtBlock apply(CtType targetType) { CtClass c = Substitution.getTemplateCtClass(targetType, this); - return Substitution.substitute(targetType, this, getBlock(c)); + List> blocks = TemplateBuilder.createPattern(getBlock(c), this).substitute(targetType); + if (blocks.size() > 1) { + throw new SpoonException("BlockTemplate cannot return more then one block"); + } + return blocks.isEmpty() ? null : blocks.get(0); } public Void S() { diff --git a/src/main/java/spoon/template/ExpressionTemplate.java b/src/main/java/spoon/template/ExpressionTemplate.java index 478b210f85a..8bc04a2eaef 100644 --- a/src/main/java/spoon/template/ExpressionTemplate.java +++ b/src/main/java/spoon/template/ExpressionTemplate.java @@ -16,6 +16,10 @@ */ package spoon.template; +import java.util.List; + +import spoon.SpoonException; +import spoon.pattern.TemplateBuilder; import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtReturn; @@ -64,7 +68,14 @@ public ExpressionTemplate() { @SuppressWarnings("unchecked") public CtExpression apply(CtType targetType) { CtClass> c = Substitution.getTemplateCtClass(targetType, this); - CtBlock b = Substitution.substitute(targetType, this, getExpressionBlock(c)); + List> blocks = TemplateBuilder.createPattern(getExpressionBlock(c), this).substitute(targetType); + if (blocks.size() > 1) { + throw new SpoonException("BlockTemplate cannot return more then one block"); + } + if (blocks.isEmpty()) { + return null; + } + CtBlock b = blocks.get(0); return ((CtReturn) b.getStatements().get(0)).getReturnedExpression(); } diff --git a/src/main/java/spoon/template/StatementTemplate.java b/src/main/java/spoon/template/StatementTemplate.java index 624b4577ba8..0a657ac28c3 100644 --- a/src/main/java/spoon/template/StatementTemplate.java +++ b/src/main/java/spoon/template/StatementTemplate.java @@ -19,10 +19,10 @@ import java.util.List; import spoon.SpoonException; +import spoon.pattern.TemplateBuilder; import spoon.reflect.code.CtStatement; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtType; -import spoon.support.template.SubstitutionVisitor; /** * This class represents a template parameter that defines a statement list @@ -45,8 +45,8 @@ public StatementTemplate() { public CtStatement apply(CtType targetType) { CtClass c = Substitution.getTemplateCtClass(targetType, this); // we substitute the first statement of method statement - CtStatement result = c.getMethod("statement").getBody().getStatements().get(0).clone(); - List statements = new SubstitutionVisitor(c.getFactory(), targetType, this).substitute(result); + CtStatement patternModel = c.getMethod("statement").getBody().getStatements().get(0); + List statements = TemplateBuilder.createPattern(patternModel, this).substitute(targetType); if (statements.size() > 1) { throw new SpoonException("StatementTemplate cannot return more then one statement"); } diff --git a/src/main/java/spoon/template/Substitution.java b/src/main/java/spoon/template/Substitution.java index b60f1ad6028..2511c8d4c1a 100644 --- a/src/main/java/spoon/template/Substitution.java +++ b/src/main/java/spoon/template/Substitution.java @@ -17,6 +17,8 @@ package spoon.template; import spoon.SpoonException; +import spoon.pattern.PatternBuilder; +import spoon.pattern.TemplateBuilder; import spoon.processing.FactoryAccessor; import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtExpression; @@ -38,7 +40,6 @@ import spoon.reflect.visitor.Query; import spoon.reflect.visitor.filter.ReferenceTypeFilter; import spoon.support.template.Parameters; -import spoon.support.template.SubstitutionVisitor; import java.lang.reflect.Field; import java.util.HashMap; @@ -113,7 +114,22 @@ public static > T createTypeFromTemplate(String qualifiedTyp CtPackage targetPackage = f.Package().getOrCreate(typeRef.getPackage().getSimpleName()); final Map extendedParams = new HashMap(templateParameters); extendedParams.put(templateOfType.getSimpleName(), typeRef); - List> generated = (List) new SubstitutionVisitor(f, extendedParams).substitute(templateOfType.clone()); + List> generated = PatternBuilder + .createPattern(templateOfType) + .configureTemplateParameters(templateOfType, extendedParams) + .configureParameters(pb -> { + templateParameters.forEach((paramName, paramValue) -> { + if (pb.isSubstituted(paramName) == false) { + if (paramValue instanceof CtTypeReference) { + CtTypeReference typeRefParamValue = (CtTypeReference) paramValue; + pb.parameter(paramName).byLocalType(templateOfType, paramName); + } + pb.parameter(paramName).bySubstring(paramName); + } + }); + }) + .build() + .substitute(extendedParams); for (CtType ctType : generated) { targetPackage.addType(ctType); } @@ -532,8 +548,7 @@ public static E substitute(CtType targetType, Template< if (targetType == null) { throw new RuntimeException("target is null in substitution"); } - E result = (E) code.clone(); - List results = new SubstitutionVisitor(targetType.getFactory(), targetType, template).substitute(result); + List results = TemplateBuilder.createPattern(code, template).substitute(targetType); if (results.size() > 1) { throw new SpoonException("StatementTemplate cannot return more then one statement"); } @@ -581,10 +596,16 @@ public static E substitute(CtType targetType, Template< * substituted */ public static > T substitute(Template template, T templateType) { - T result = (T) templateType.clone(); - result.setPositions(null); // result.setParent(templateType.getParent()); - new SubstitutionVisitor(templateType.getFactory(), result, template).substitute(result); + List types = TemplateBuilder.createPattern(templateType, template).substitute(null); + if (types.size() > 1) { + throw new SpoonException("Cannot generate more then one type"); + } + if (types.isEmpty()) { + return null; + } + T result = types.get(0); + result.setPositions(null); return result; } @@ -657,7 +678,7 @@ static CtClass getTemplateCtClass(CtType targetType, Template templ * * @return - CtClass from the already built spoon model, which represents the template */ - static CtClass getTemplateCtClass(Factory factory, Template template) { + public static CtClass getTemplateCtClass(Factory factory, Template template) { CtClass c = factory.Class().get(template.getClass()); if (c.isShadow()) { throw new SpoonException("The template " + template.getClass().getName() + " is not part of model. Add template sources to spoon template path."); diff --git a/src/main/java/spoon/template/TemplateMatcher.java b/src/main/java/spoon/template/TemplateMatcher.java index cd9730aa495..013bb0653e1 100644 --- a/src/main/java/spoon/template/TemplateMatcher.java +++ b/src/main/java/spoon/template/TemplateMatcher.java @@ -18,21 +18,26 @@ import spoon.Launcher; import spoon.SpoonException; -import spoon.reflect.code.CtBlock; +import spoon.pattern.AbstractAttributeSubstitutionRequest; +import spoon.pattern.AbstractNodeSubstitutionRequest; +import spoon.pattern.NodeAttributesSubstitionRequest; +import spoon.pattern.NodeSubstitutionRequest; +import spoon.pattern.ParameterInfo; +import spoon.pattern.TemplateBuilder; import spoon.reflect.code.CtFieldAccess; import spoon.reflect.code.CtInvocation; -import spoon.reflect.code.CtStatement; import spoon.reflect.code.CtStatementList; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtField; import spoon.reflect.declaration.CtNamedElement; import spoon.reflect.declaration.CtParameter; -import spoon.reflect.declaration.CtVariable; import spoon.reflect.declaration.ParentNotInitializedException; +import spoon.reflect.meta.RoleHandler; +import spoon.reflect.meta.impl.RoleHandlerHelper; +import spoon.reflect.path.CtRole; import spoon.reflect.reference.CtExecutableReference; import spoon.reflect.reference.CtFieldReference; -import spoon.reflect.reference.CtPackageReference; import spoon.reflect.reference.CtReference; import spoon.reflect.reference.CtTypeParameterReference; import spoon.reflect.reference.CtTypeReference; @@ -43,18 +48,15 @@ import spoon.support.template.DefaultParameterMatcher; import spoon.support.template.ParameterMatcher; import spoon.support.template.Parameters; -import spoon.support.util.RtHelper; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Set; /** * This class defines an engine for matching a template to pieces of code. @@ -143,9 +145,15 @@ private List> getVarargs(CtClass> root private CtElement templateRoot; /** - * Holds matches of template parameters (keys) to nodes from matched target + * Holds matches of template parameters name to matching values. + * The values can be: + *
  • + *
      single CtElement + *
        list or set of CtElements + *
          any value of primitive attribute, like String, Enum value, number, ... + * */ - private Map matches = new HashMap<>(); + private Map matches = new HashMap<>(); /** * Names of all template parameters declared in `templateType` and it's super types/interfaces. @@ -162,12 +170,6 @@ private List> getVarargs(CtClass> root */ private CtClass> templateType; - /** - * All the {@link CtTypeReference}s from `templateType`, whose name is a parameter name - * (is contained in `names`) - */ - private List> typeVariables; - /** * List of all fields of type {@link CtStatementList}, * which are not covered by `variables` @@ -179,86 +181,63 @@ private List> getVarargs(CtClass> root */ private List> variables; + private spoon.pattern.Pattern pattern; + + public TemplateMatcher(spoon.pattern.Pattern pattern) { + this.pattern = pattern; + } /** * Constructs a matcher for a given template. * * @param templateRoot the template to match against * */ - @SuppressWarnings("unchecked") public TemplateMatcher(CtElement templateRoot) { - this.templateType = templateRoot.getParent(CtClass.class); + this(templateRoot, templateRoot.getParent(CtClass.class)); + } + + public TemplateMatcher(CtElement templateRoot, CtClass templateType) { + this.pattern = TemplateBuilder.createPattern(templateRoot, templateType, null).build(); +// CtClass templateType = templateRoot.getParent(CtClass.class); this.templateRoot = templateRoot; - variables = getMethods(templateType); - typeVariables = getTemplateTypeParameters(templateType); - names = getTemplateNameParameters(templateType); - varArgs = getVarargs(templateType, variables); +// variables = getMethods(templateType); +// names = getTemplateNameParameters(templateType); +// varArgs = getVarargs(templateType, variables); //check that template matches itself - if (helperMatch(this.templateRoot, this.templateRoot) == false) { + if (matchElement(this.templateRoot, this.pattern.getModel()) == false) { throw new SpoonException("TemplateMatcher was unable to find itself, it certainly indicates a bug. Please revise your template or report an issue."); } } - /** - * adds a target element which matches and template element - * @param template an object template. It can be: - * - CtInvocation - represents an variable - * - CtTypeReference - represents an type variable - * - String - represents a matching name in a reference - * - CtParameter - ?? - * - ...? - * @param target an matching target object - * @return false if there was already a different match to the same `template` object - */ - private boolean addMatch(Object template, Object target) { - Object inv = matches.get(template); - Object o = matches.put(template, target); - /* - * BUG: it always returns true, because inv==o. It is contract of Map. - * The correct code is probably: - * - * Object inv = matches.get(template); - * if (inv != null && inv.equals(target) == false) { - * //another value would be inserted. TemplateMatcher does not support matching of different values for the same template parameter - * return false; - * } - * matches.put(template, target) - * return true; - * Object inv = matches.put(template, target); - * return (null == inv) || inv.equals(target); - * - * But callers of addMatch does not handle return value consistently to this contract ... - */ - return (null == inv) || inv.equals(o); - } - - /** - * Detects whether `teList` contains a multiElement template parameter - * @param teList a list of template nodes - * @return a first found multiElement template parameter - */ - private CtElement checkListStatements(List teList) { - for (Object tem : teList) { - //TODO: simplify, if it is same like an item of variables, then it must be a CtInvocation - if (containsSame(variables, tem) && (tem instanceof CtInvocation)) { - CtInvocation listCand = (CtInvocation) tem; - //BUG: it returns true only for parameters of type TemplateParameter, because interface TemplateParameter can never be a subtype of something else - boolean ok = listCand.getFactory().Type().createReference(TemplateParameter.class).isSubtypeOf(listCand.getTarget().getType()); - return ok ? listCand : null; - } - if (tem instanceof CtVariable) { - CtVariable var = (CtVariable) tem; - String name = var.getSimpleName(); - for (CtFieldReference f : varArgs) { - if (f.getSimpleName().equals(name)) { - return f.getDeclaration(); - } - } - } - } - - return null; - } +// /** +// * Detects whether `teList` contains a multiElement template parameter +// * @param context TODO +// * @param teList a list of template nodes +// * @return a first found multiElement template parameter +// */ +// private CtElement checkListStatements(MatchingContext context, List teList) { +// for (CtElement tem : teList) { +// AbstractNodeSubstitutionRequest req = pattern.getSubstitutionRequest(tem); +// //TODO: simplify, if it is same like an item of variables, then it must be a CtInvocation +// if (containsSame(variables, tem) && (tem instanceof CtInvocation)) { +// CtInvocation listCand = (CtInvocation) tem; +// //BUG: it returns true only for parameters of type TemplateParameter, because interface TemplateParameter can never be a subtype of something else +// boolean ok = listCand.getFactory().Type().createReference(TemplateParameter.class).isSubtypeOf(listCand.getTarget().getType()); +// return ok ? listCand : null; +// } +// if (tem instanceof CtVariable) { +// CtVariable var = (CtVariable) tem; +// String name = var.getSimpleName(); +// for (CtFieldReference f : varArgs) { +// if (f.getSimpleName().equals(name)) { +// return f.getDeclaration(); +// } +// } +// } +// } +// +// return null; +// } /** * Finds all target program sub-trees that correspond to a template. @@ -341,8 +320,8 @@ public void visitCtField(CtField f) { * template parameters. The {@link #match(CtElement, CtElement)} method must * have been called before. */ - private Map getMatches() { - return matches; + public Map getMatches() { + return new HashMap<>(matches); } /** returns a specific ParameterMatcher corresponding to the field acting as template parameter */ @@ -366,7 +345,6 @@ private ParameterMatcher getParameterInstance(CtFieldReference param) throws * Detects whether `template` AST node and `target` AST node are matching. * This method is called for each node of to be matched template * and for appropriate node of `target` - * * @param target actually checked AST node from target model * @param template actually checked AST node from template * @@ -374,145 +352,71 @@ private ParameterMatcher getParameterInstance(CtFieldReference param) throws * * note: Made private to hide the Objects. */ - private boolean helperMatch(Object target, Object template) { - if ((target == null) && (template == null)) { + private boolean matchElement(CtElement target, CtElement template) { + if (target == template) { return true; } - if ((target == null) || (template == null)) { + if (target == null || template == null) { return false; } - if (containsSame(variables, template) || containsSame(typeVariables, template)) { + AbstractNodeSubstitutionRequest substRequest = pattern.getSubstitutionRequest((CtElement) template); + if (substRequest instanceof NodeSubstitutionRequest) { + NodeSubstitutionRequest nodeSubstReq = (NodeSubstitutionRequest) substRequest; /* - * we are just matching a template parameter. + * Pattern substitutes whole node here. * Check that defined ParameterMatcher matches the target too */ - boolean add = invokeCallBack(target, template); - if (add) { - //ParameterMatcher matches the target too, add that match - //BUG: if addMatch returns false, then report it as - //Launcher.LOGGER.debug("incongruent match"); - return addMatch(template, target); - } - return false; - } + return nodeSubstReq.matches(this, (CtElement) target, (CtElement) template); + } //else there is no substitution request or substition request for a specific attribute of this node + + //both classes must be same if (target.getClass() != template.getClass()) { return false; } - if ((template instanceof CtTypeReference) && template.equals(templateType.getReference())) { - return true; - } - if ((template instanceof CtPackageReference) && template.equals(templateType.getPackage())) { - return true; - } - if (template instanceof CtReference) { - CtReference tRef = (CtReference) template; - /* - * Check whether name of a template reference matches with name of target reference - * after replacing of variables in template name - */ - boolean ok = matchNames(tRef.getSimpleName(), ((CtReference) target).getSimpleName()); - /* - * TODO comment: In what case the template.equals(target) == true?? - */ - if (ok && !template.equals(target)) { - boolean remove = !invokeCallBack(target, template); - if (remove) { - /* - * BUG: if ParameterMatcher does not agrees then it should remove a match, - * but the match was inserted with different key by matchNames! - * The best solution would be to add the match only after it is agreed by ParameterMatcher. - * It avoids replacing of correct match by incorrect match in `matches` - */ - matches.remove(tRef.getSimpleName()); - return false; - } - return true; - } - } - if (template instanceof CtNamedElement) { - /* - * same code like above, with same bugs - * TODO use a shared function called from both places and fix it once. - */ - CtNamedElement named = (CtNamedElement) template; - boolean ok = matchNames(named.getSimpleName(), ((CtNamedElement) target).getSimpleName()); - if (ok && !template.equals(target)) { - boolean remove = !invokeCallBack(target, template); - if (remove) { - matches.remove(named.getSimpleName()); - return false; - } - } + if (template instanceof CtTypeReference) { + CtTypeReference templateTypeRef = (CtTypeReference) template; + CtTypeReference targetTypeRef = (CtTypeReference) target; + //target type reference must be equal or a sub class of template type ref + return targetTypeRef.isSubtypeOf(templateTypeRef); } - if (template instanceof Collection) { - return matchCollections((Collection) target, (Collection) template); - } + //may be null + NodeAttributesSubstitionRequest nodeAttrsSubstReq = (NodeAttributesSubstitionRequest) substRequest; - if (template instanceof Map) { - if (template.equals(target)) { - return true; - } - - Map temMap = (Map) template; - Map tarMap = (Map) target; - - if (!temMap.keySet().equals(tarMap.keySet())) { + for (RoleHandler roleHandler : RoleHandlerHelper.getRoleHandlers(((CtElement) target).getClass())) { + if (matchElementAttribute(nodeAttrsSubstReq, roleHandler, target, template) == false) { return false; } - - return matchCollections(tarMap.values(), temMap.values()); - } - - if (template instanceof CtBlock) { - final List statements = ((CtBlock) template).getStatements(); - if (statements.size() == 1 && statements.get(0) instanceof CtInvocation) { - final CtInvocation ctStatement = (CtInvocation) statements.get(0); - if ("S".equals(ctStatement.getExecutable().getSimpleName()) && CtBlock.class.equals(ctStatement.getType().getActualClass())) { - return true; - } - } } + return true; + } - if (target instanceof CtElement) { - //TODO cache relevant fields for a spoon model class in a static Map - for (Field f : RtHelper.getAllFields(target.getClass())) { - f.setAccessible(true); - if (Modifier.isStatic(f.getModifiers())) { - continue; - } - if (f.getName().equals("parent")) { - continue; - } - if (f.getName().equals("position")) { - continue; - } - if (f.getName().equals("docComment")) { - continue; - } - if (f.getName().equals("factory")) { - continue; - } - if (f.getName().equals("comments")) { - continue; - } - if (f.getName().equals("metadata")) { - continue; + private boolean matchElementAttribute(NodeAttributesSubstitionRequest nodeAttrsSubstReq, RoleHandler roleHandler, Object target, Object template) { + if (isMatchingRole(roleHandler.getRole())) { + //this role has to be checked + AbstractAttributeSubstitutionRequest attrSubstReq = nodeAttrsSubstReq == null ? null : nodeAttrsSubstReq.getAttributeSubstititionRequest(roleHandler.getRole()); + if (attrSubstReq != null) { + if (attrSubstReq.matches(this, roleHandler.getValue(target), roleHandler.getValue(template)) == false) { + return false; } - try { - if (!helperMatch(f.get(target), f.get(template))) { - return false; - } - } catch (IllegalAccessException ignore) { + } else { + if (matches(roleHandler, roleHandler.getValue(target), roleHandler.getValue(template)) == false) { + return false; } } + } //else role has to be ignored + return true; + } + + private boolean objectsEqual(Object a, Object b) { + if (a == b) { return true; - } else if (target instanceof String) { - return matchNames((String) template, (String) target); - } else { - return target.equals(template); } + if (a == null) { + return false; + } + return a.equals(b); } /** @@ -523,7 +427,7 @@ private boolean helperMatch(Object target, Object template) { * * TODO: rename this method to #checkParameterMatcher */ - private boolean invokeCallBack(Object target, Object template) { + private boolean invokeCallBack(Object target, Object template, ParameterInfo parameterInfo) { try { if (template instanceof CtInvocation) { CtFieldAccess param = (CtFieldAccess) ((CtInvocation) template).getTarget(); @@ -591,200 +495,324 @@ public boolean matches(CtElement targetRoot) { // Correct template matches itself of course, but client does not want that return false; } - return helperMatch(targetRoot, templateRoot); + //clear all matches from previous run before we start matching with `targetRoot` + matches.clear(); + boolean found = matchElement(targetRoot, templateRoot); + return found; } - @SuppressWarnings("unchecked") - private boolean matchCollections(Collection target, Collection template) { - final List teList = new ArrayList<>(template); - final List taList = new ArrayList<>(target); - - // inMulti keeps the multiElement templateVariable we are at - CtElement inMulti = nextListStatement(teList, null); + public Object getMatch(String paramName) { + return matches.get(paramName); + } - // multi keeps the values to assign to inMulti - List multi = new ArrayList<>(); + public boolean addMatch(ParameterInfo param, Object value) { + Object inv = matches.put(param.getName(), value); + if (inv != null && inv.equals(value) == false) { + // another value would be inserted. TemplateMatcher does not support + // matching of different values for the same template parameter + Launcher.LOGGER.debug("incongruent match on parameter " + param.getName() + " with value " + value); + return false; + } + return true; + } - if (null == inMulti) { - // If we are not looking at template with multiElements - // the sizes should then be the same - if (teList.size() != taList.size()) { + public boolean isMatchingRole(CtRole role) { + switch (role) { + case COMMENT: + case POSITION: return false; - } - //TODO simplify the cycle. Use one index for both lists - for (int te = 0, ta = 0; (te < teList.size()) && (ta < taList.size()); te++, ta++) { - if (!helperMatch(taList.get(ta), teList.get(te))) { - return false; - } - } + default: + //match on super roles only. Ignore derived roles + return role.getSuperRole() == null; + } + } + + public boolean matches(RoleHandler roleHandler, Object target, Object template) { + if (template == target) { return true; } - for (int te = 0, ta = 0; (te < teList.size()) && (ta < taList.size()); te++, ta++) { - - if (isCurrentTemplate(teList.get(te), inMulti)) { - //te index points to template parameter, which accepts multiple statements - if (te + 1 >= teList.size()) { - //it is the last parameter of template list. Add all remaining target list items - multi.addAll(taList.subList(te, taList.size())); - //create statement list and add match - CtStatementList tpl = templateType.getFactory().Core().createStatementList(); - tpl.setStatements((List) (List) multi); - if (!invokeCallBack(tpl, inMulti)) { - return false; - } - boolean ret = addMatch(inMulti, multi); - //BUG: if addMatch returns false, then report it as - //Launcher.LOGGER.debug("incongruent match"); - return ret; - } - //there is next template parameter. Move to it - te++; - //adds all target list items, which are not matching to next template parameter, to the actual template parameter - /* - * IMPROVE: - * - do not check (te < teList.size()), because it is already tested above - * - get teList.get(te) into local variable - * then it will be clear that this cycle iterates over taList only - */ - while ((te < teList.size()) && (ta < taList.size()) && !helperMatch(taList.get(ta), teList.get(te))) { - multi.add(taList.get(ta)); - ta++; - } - //BUG: te-- ?? Or do not increase te above at all? + if (CtElement.class.isAssignableFrom(roleHandler.getValueClass())) { + //the items are CtElement instances + switch (roleHandler.getContainerKind()) { + case LIST: + return matchesList(roleHandler, (List) target, (List) template); + case SET: + return matchesSet(roleHandler, (Set) target, (Set) template); + case MAP: + return matchesMap(roleHandler, (Map) target, (Map) template); + case SINGLE: + return matchElement((CtElement) target, (CtElement) template); + } + throw new SpoonException("Unexpected RoleHandler containerKind: " + roleHandler.getContainerKind()); + } + /* + * the items are other Objects. + * Use equals on Object, List, Set or Map + */ + return objectsEqual(target, template); + } - //we have found first target parameter, which fits to next template parameter - //create statement list for previous parameter and add it's match - CtStatementList tpl = templateType.getFactory().Core().createStatementList(); - tpl.setStatements((List) (List) multi); - if (!invokeCallBack(tpl, inMulti)) { + private boolean matchesList(RoleHandler roleHandler, List target, List template) { + if (target == null) { + target = Collections.emptyList(); + } + if (template == null) { + template = Collections.emptyList(); + } + Iterator targetIter = target.iterator(); + Iterator templateIter = template.iterator(); + while (templateIter.hasNext()) { + CtElement templateItem = templateIter.next(); + AbstractNodeSubstitutionRequest substReq = pattern.getSubstitutionRequest(templateItem); + if (substReq instanceof NodeSubstitutionRequest) { + NodeSubstitutionRequest nodeSubstRequest = (NodeSubstitutionRequest) substReq; + List myMatches = new ArrayList<>(); + if (nodeSubstRequest.matches(this, myMatches, templateItem) == false) { return false; } - //BUG: why we do not care about return value here? - // if addMatch returns false, then report it as - //Launcher.LOGGER.debug("incongruent match"); - addMatch(inMulti, tpl); - // update inMulti - inMulti = nextListStatement(teList, inMulti); - multi = new ArrayList<>(); + while (targetIter.hasNext()) { + CtElement targetItem = targetIter.next(); + //TODO detect how many items has to be added here + myMatches.add(targetItem); + } } else { - //parameter on te index is not a multivalue statement - if (!helperMatch(taList.get(ta), teList.get(te))) { + if (targetIter.hasNext() == false) { + //template has element, but target hasn't return false; } - //TODO: make condition more readable. E.g. ta+1>=taList.size() - if (!(ta + 1 < taList.size()) && (inMulti != null)) { - /* - * there is no next target item in taList, - * but there is still some template parameter, - * which expects one - */ - CtStatementList tpl = templateType.getFactory().Core().createStatementList(); - //BUG: it looks like `multi` must be always empty - //TODO: delete this cycle - for (Object o : multi) { - tpl.addStatement((CtStatement) o); - } - //so it returns empty statement list - might be OK - if (!invokeCallBack(tpl, inMulti)) { - return false; - } - //BUG: why we do not care about return value here? - // if addMatch returns false, then report it as - //Launcher.LOGGER.debug("incongruent match"); - addMatch(inMulti, tpl); - // update inMulti - inMulti = nextListStatement(teList, inMulti); - multi = new ArrayList<>(); - /* - * BUG: if there is next `inMulti` template parameter, - * then it is not checked whether it matches empty statement list, - * because ta+1==taList.size() and it finishes the main cycle. - */ + CtElement targetItem = targetIter.next(); + if (matchElement(targetItem, templateItem) == false) { + return false; } } } return true; } - /** - * Detects if `templateName` (a name from template) matches with `elementName` (a name from target), - * after replacing parameter names in `templateName` - * @param templateName the name from template - * @param elementName the name from target - * @return true if matching - * - * TODO fix BUG: refactor this method and callers, to call addMatch correctly - */ - private boolean matchNames(String templateName, String elementName) { - - for (String templateParameterName : names) { - // pname = pname.replace("_FIELD_", ""); - if (templateName.contains(templateParameterName)) { - String newName = templateName.replace(templateParameterName, "(.*)"); - Pattern p = Pattern.compile(newName); - Matcher m = p.matcher(elementName); - if (!m.matches()) { - return false; - } - // TODO: fix with parameter from @Parameter - // boolean ok = addMatch(getBindedParameter(pname), - // m.group(1)); - boolean ok = addMatch(templateParameterName, m.group(1)); - if (!ok) { - Launcher.LOGGER.debug("incongruent match"); - return false; - } - return true; - } - } - return templateName.equals(elementName); + private boolean matchesSet(RoleHandler roleHandler, Set target, Set template) { + if (target == null) { + target = Collections.emptySet(); + } + if (template == null) { + template = Collections.emptySet(); + } + //TODO ask roleHandler for the unique key of the values in set + return false; } - /** - * returns next ListStatement parameter from teList - * - * BUG: it works only for first and second parameter. The 3rd call will return first parameter again! - * - * @param teList - * @param inMulti TODO replace by int index - * @return TODO return int index of found statement or -1 if there is no next one - */ - private CtElement nextListStatement(List teList, CtElement inMulti) { - if (inMulti == null) { - return checkListStatements(teList); - } - List teList2 = new ArrayList(teList); - if (inMulti instanceof CtInvocation) { - //BUG: we should use removeSame, which uses "==" instead of "equals" - teList2.remove(inMulti); - } else if (inMulti instanceof CtVariable) { - CtVariable var = (CtVariable) inMulti; - for (Iterator iter = teList2.iterator(); iter.hasNext();) { - CtVariable teVar = (CtVariable) iter.next(); - if (teVar.getSimpleName().equals(var.getSimpleName())) { - iter.remove(); - //BUG? Should it really remove all variables of the same name, or should it remove first found? - } - } + private boolean matchesMap(RoleHandler roleHandler, Map target, Map template) { + if (target == null) { + target = Collections.emptyMap(); + } + if (template == null) { + template = Collections.emptyMap(); } - return checkListStatements(teList2); - } - /** - * Is used instead of Collection#contains(Object), - * which uses Object#equals operator, - * which returns true even for not same objects. - * - * @param collection to be checked collection - * @param item to be searched object - * @return true if `collection` contains instance of `item`. - */ - private static boolean containsSame(Iterable collection, Object item) { - for (Object object : collection) { - if (object == item) { - return true; + if (template.keySet().equals(target.keySet()) == false) { + return false; + } + + for (Map.Entry templateEntry : template.entrySet()) { + CtElement targetValue = target.get(templateEntry.getKey()); + if (matchElement(targetValue, templateEntry.getValue()) == false) { + return false; } } - return false; + return true; } + +// @SuppressWarnings("unchecked") +// private boolean matchCollections(MatchingContext context, Collection target, Collection template) { +// final List teList = new ArrayList<>(template); +// final List taList = new ArrayList<>(target); +// +// // inMulti keeps the multiElement templateVariable we are at +// CtElement inMulti = nextListStatement(context, teList, null); +// +// // multi keeps the values to assign to inMulti +// List multi = new ArrayList<>(); +// +// if (null == inMulti) { +// // If we are not looking at template with multiElements +// // the sizes should then be the same +// if (teList.size() != taList.size()) { +// return false; +// } +// //TODO simplify the cycle. Use one index for both lists +// for (int te = 0, ta = 0; (te < teList.size()) && (ta < taList.size()); te++, ta++) { +// if (!helperMatch((CtElement) taList.get(ta), (CtElement) teList.get(te))) { +// return false; +// } +// } +// return true; +// } +// for (int te = 0, ta = 0; (te < teList.size()) && (ta < taList.size()); te++, ta++) { +// +// if (isCurrentTemplate(teList.get(te), inMulti)) { +// //te index points to template parameter, which accepts multiple statements +// if (te + 1 >= teList.size()) { +// //it is the last parameter of template list. Add all remaining target list items +// multi.addAll(taList.subList(te, taList.size())); +// //create statement list and add match +// CtStatementList tpl = templateType.getFactory().Core().createStatementList(); +// tpl.setStatements((List) (List) multi); +// if (!invokeCallBack(tpl, inMulti)) { +// return false; +// } +// boolean ret = addMatch(inMulti, multi); +// //BUG: if addMatch returns false, then report it as +// //Launcher.LOGGER.debug("incongruent match"); +// return ret; +// } +// //there is next template parameter. Move to it +// te++; +// //adds all target list items, which are not matching to next template parameter, to the actual template parameter +// /* +// * IMPROVE: +// * - do not check (te < teList.size()), because it is already tested above +// * - get teList.get(te) into local variable +// * then it will be clear that this cycle iterates over taList only +// */ +// while ((te < teList.size()) && (ta < taList.size()) && !helperMatch(taList.get(ta), teList.get(te))) { +// multi.add(taList.get(ta)); +// ta++; +// } +// //BUG: te-- ?? Or do not increase te above at all? +// +// //we have found first target parameter, which fits to next template parameter +// //create statement list for previous parameter and add it's match +// CtStatementList tpl = templateType.getFactory().Core().createStatementList(); +// tpl.setStatements((List) (List) multi); +// if (!invokeCallBack(tpl, inMulti)) { +// return false; +// } +// //BUG: why we do not care about return value here? +// // if addMatch returns false, then report it as +// //Launcher.LOGGER.debug("incongruent match"); +// addMatch(inMulti, tpl); +// // update inMulti +// inMulti = nextListStatement(context, teList, inMulti); +// multi = new ArrayList<>(); +// } else { +// //parameter on te index is not a multivalue statement +// if (!helperMatch(taList.get(ta), teList.get(te))) { +// return false; +// } +// //TODO: make condition more readable. E.g. ta+1>=taList.size() +// if (!(ta + 1 < taList.size()) && (inMulti != null)) { +// /* +// * there is no next target item in taList, +// * but there is still some template parameter, +// * which expects one +// */ +// CtStatementList tpl = templateType.getFactory().Core().createStatementList(); +// //BUG: it looks like `multi` must be always empty +// //TODO: delete this cycle +// for (Object o : multi) { +// tpl.addStatement((CtStatement) o); +// } +// //so it returns empty statement list - might be OK +// if (!invokeCallBack(tpl, inMulti)) { +// return false; +// } +// //BUG: why we do not care about return value here? +// // if addMatch returns false, then report it as +// //Launcher.LOGGER.debug("incongruent match"); +// addMatch(inMulti, tpl); +// // update inMulti +// inMulti = nextListStatement(context, teList, inMulti); +// multi = new ArrayList<>(); +// /* +// * BUG: if there is next `inMulti` template parameter, +// * then it is not checked whether it matches empty statement list, +// * because ta+1==taList.size() and it finishes the main cycle. +// */ +// } +// } +// } +// return true; +// } +// +// /** +// * Detects if `templateName` (a name from template) matches with `elementName` (a name from target), +// * after replacing parameter names in `templateName` +// * @param templateName the name from template +// * @param elementName the name from target +// * @return true if matching +// * +// * TODO fix BUG: refactor this method and callers, to call addMatch correctly +// */ +// private boolean matchNames(String templateName, String elementName) { +// +// for (String templateParameterName : names) { +// // pname = pname.replace("_FIELD_", ""); +// if (templateName.contains(templateParameterName)) { +// String newName = templateName.replace(templateParameterName, "(.*)"); +// Pattern p = Pattern.compile(newName); +// Matcher m = p.matcher(elementName); +// if (!m.matches()) { +// return false; +// } +// // TODO: fix with parameter from @Parameter +// // boolean ok = addMatch(getBindedParameter(pname), +// // m.group(1)); +// boolean ok = addMatch(templateParameterName, m.group(1)); +// if (!ok) { +// Launcher.LOGGER.debug("incongruent match"); +// return false; +// } +// return true; +// } +// } +// return templateName.equals(elementName); +// } +// +// /** +// * returns next ListStatement parameter from teList +// * +// * BUG: it works only for first and second parameter. The 3rd call will return first parameter again! +// * @param context TODO +// * @param teList +// * @param inMulti TODO replace by int index +// * +// * @return TODO return int index of found statement or -1 if there is no next one +// */ +// private CtElement nextListStatement(MatchingContext context, List teList, CtElement inMulti) { +// if (inMulti == null) { +// return checkListStatements(context, teList); +// } +// List teList2 = new ArrayList<>(teList); +// if (inMulti instanceof CtInvocation) { +// //BUG: we should use removeSame, which uses "==" instead of "equals" +// teList2.remove(inMulti); +// } else if (inMulti instanceof CtVariable) { +// CtVariable var = (CtVariable) inMulti; +// for (Iterator iter = teList2.iterator(); iter.hasNext();) { +// CtVariable teVar = (CtVariable) iter.next(); +// if (teVar.getSimpleName().equals(var.getSimpleName())) { +// iter.remove(); +// //BUG? Should it really remove all variables of the same name, or should it remove first found? +// } +// } +// } +// return checkListStatements(context, teList2); +// } +// +// /** +// * Is used instead of Collection#contains(Object), +// * which uses Object#equals operator, +// * which returns true even for not same objects. +// * +// * @param collection to be checked collection +// * @param item to be searched object +// * @return true if `collection` contains instance of `item`. +// */ +// private static boolean containsSame(Iterable collection, Object item) { +// for (Object object : collection) { +// if (object == item) { +// return true; +// } +// } +// return false; +// } } diff --git a/src/test/java/spoon/test/prettyprinter/templates/NewPattern.java b/src/test/java/spoon/test/prettyprinter/templates/NewPattern.java new file mode 100644 index 00000000000..d45f9044d11 --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/templates/NewPattern.java @@ -0,0 +1,65 @@ +package spoon.test.prettyprinter.templates; + +import java.util.Map; +import java.util.function.Consumer; + +import spoon.pattern.Pattern; +import spoon.pattern.PatternBuilder; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.factory.Factory; +import spoon.template.TemplateMatcher; +import spoon.test.prettyprinter.templates.OldPattern.Entity; +import spoon.test.prettyprinter.templates.OldPattern.Item; +import spoon.test.prettyprinter.templates.OldPattern.ListPrinter; +import spoon.test.prettyprinter.templates.OldPattern.Printer; + +public class NewPattern { + + Entity entity; + String start; + String next; + String end; + + + ElementPrinterHelper elementPrinterHelper; + + /** + * The body of this method contains new pattern model + */ + private void patternModel() throws Exception { + elementPrinterHelper.printList(entity.$getItems$(), start, next, end, v -> statements()); + } + + static Pattern create(Factory factory) { + return PatternBuilder + .createStatementsPattern(factory.Class().get(NewPattern.class), exec->exec.getSimpleName().equals("patternModel")) + .configureParameters(pb->pb + .parameter("entityType").byType(Entity.class) + .parameter("methodName").bySubstring("$getItems$")) + .configureAutomaticParameters() + .build(); + } + + static void replaceOldByNew(Factory f) { + Pattern newPattern = NewPattern.create(f); + TemplateMatcher tm = new TemplateMatcher(OldPattern.create(f)); + f.getModel().getRootPackage().filterChildren(tm).forEach((CtElement match) -> { + Map params = tm.getMatches(); + match.replace(newPattern.substitute(params)); + }); + } + + void statements() { + } + + interface Item { + } + + interface Entity { + Iterable $getItems$(); + } + + interface ElementPrinterHelper { + void printList(Iterable $getItems$, String start, String next, String end, Consumer consumer); + } +} diff --git a/src/test/java/spoon/test/prettyprinter/templates/OldPattern.java b/src/test/java/spoon/test/prettyprinter/templates/OldPattern.java new file mode 100644 index 00000000000..5d6c0a41539 --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/templates/OldPattern.java @@ -0,0 +1,69 @@ +package spoon.test.prettyprinter.templates; + +import spoon.pattern.Pattern; +import spoon.pattern.PatternBuilder; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.factory.Factory; + +public class OldPattern { + + + void statements(Entity entity) throws Exception { + if(useStartKeyword) { + printer.writeSpace().writeKeyword(startKeyword).writeSpace(); + } + try (ListPrinter lp = printer.createListPrinter(start, next, end)) { + for (Item value : entity.$getItems$()) { + lp.printSeparatorIfAppropriate(); + statements(); + } + } + } + + static Pattern create(Factory factory) { + return PatternBuilder + .createStatementsPattern(factory.Class().get(OldPattern.class), exec->exec.getSimpleName().equals("statements")) + .configureParameters(pb->pb + .parameter("entityType").byType(Entity.class).matchCondition(e->e instanceof CtElement) + .parameter("itemType").byType(Item.class).matchCondition(e->e instanceof CtElement) + .parameter("listPrinterType").byType(ListPrinter.class).matchCondition(e->e instanceof spoon.reflect.visitor.ListPrinter) + .parameter("methodName").bySubstring("$getItems$") + ) + .build(); + } + + boolean useStartKeyword; + String startKeyword; + String start; + String next; + String end; + + void statements() { + } + + interface Item { + } + + interface Entity { + Iterable $getItems$(); + } + + Printer printer; + + class ListPrinter implements AutoCloseable { + + @Override + public void close() throws Exception { + } + + public void printSeparatorIfAppropriate() { + } + } + + interface Printer { + ListPrinter createListPrinter(String p1, String p2, String p3); + + Printer writeSpace(); + Printer writeKeyword(String s); + } +} diff --git a/src/test/java/spoon/test/prettyprinter/templates/OldPattern_TemplateParameters.java b/src/test/java/spoon/test/prettyprinter/templates/OldPattern_TemplateParameters.java new file mode 100644 index 00000000000..e6da721fc7f --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/templates/OldPattern_TemplateParameters.java @@ -0,0 +1,78 @@ +package spoon.test.prettyprinter.templates; + +import spoon.pattern.Pattern; +import spoon.pattern.PatternBuilder; +import spoon.reflect.code.CtBlock; +import spoon.reflect.code.CtExpression; +import spoon.reflect.code.CtLiteral; +import spoon.reflect.factory.Factory; +import spoon.reflect.reference.CtTypeReference; +import spoon.support.reflect.code.CtBlockImpl; +import spoon.support.reflect.code.CtExpressionImpl; +import spoon.support.reflect.code.CtLiteralImpl; +import spoon.template.Local; +import spoon.template.Parameter; +import spoon.template.TemplateParameter; + +/** + */ +public class OldPattern_TemplateParameters { + + + void statements(Entity entity) throws Exception { + try (ListPrinter lp = printer.createListPrinter(start.S(), next.S(), end.S())) { + for (Item value : entity.$getItems$()) { + lp.printSeparatorIfAppropriate(); + statements.S(); + } + } + } + + static Pattern create(Factory factory) { + return PatternBuilder + .createStatementsPattern(factory.Class().get(OldPattern_TemplateParameters.class), exec->exec.getSimpleName().equals("statements")) + .build(); + } + + TemplateParameter start; + TemplateParameter next; + CtLiteral end; + TemplateParameter statements; + @Parameter("Entity") + CtTypeReference entityType; + @Parameter("$getItems$") + String methodName; + @Parameter("Item") + CtTypeReference itemType; + + + + @Local + interface Item { + } + + @Local + interface Entity { + Iterable $getItems$(); + } + + @Local + Printer printer; + + @Local + class ListPrinter implements AutoCloseable { + + @Override + public void close() throws Exception { + } + + public void printSeparatorIfAppropriate() { + } + } + + @Local + interface Printer { + ListPrinter createListPrinter(String p1, String p2, String p3); + } + +} diff --git a/src/test/java/spoon/test/prettyprinter/templates/OldPattern_methods.java b/src/test/java/spoon/test/prettyprinter/templates/OldPattern_methods.java new file mode 100644 index 00000000000..137e1043d9f --- /dev/null +++ b/src/test/java/spoon/test/prettyprinter/templates/OldPattern_methods.java @@ -0,0 +1,69 @@ +package spoon.test.prettyprinter.templates; + +import java.util.function.Predicate; + +import spoon.pattern.Pattern; +import spoon.pattern.PatternBuilder; +import spoon.reflect.code.CtBinaryOperator; +import spoon.reflect.code.CtReturn; +import spoon.reflect.code.CtStatement; +import spoon.reflect.factory.Factory; +import spoon.reflect.visitor.Filter; +/** + * Method can be used as expression or as statement + * + * CONS: There is no way how to dynamically fill in values of template parameters + */ +public abstract class OldPattern_methods { + + Printer printer; + + void statements(Entity entity) throws Exception { + try (ListPrinter lp = printer.createListPrinter(start(), next(), end())) { + for (Item value : entity.$getItems$()) { + lp.printSeparatorIfAppropriate(); + statements(); + } + } + } + + /** + * + * @param factory + * @return a new {@link Pattern} instance made from the statements in body of method {@link #statements()} above + */ + public static Pattern create(Factory factory) { + return PatternBuilder + .createStatementsPattern(factory.Class().get(OldPattern_methods.class), exec->exec.getSimpleName().equals("statements")) + .configureParameters(pb->pb + //declare pattern parameter named `entityType` for all type references to class `Entity` in the template model + .parameter("entityType").byType(Entity.class) + //declare pattern parameter named `methodName` for all elements whose string attribute has value "$getItems$" + .parameter("methodName").bySubstring("$getItems$") + ) + /* + * note: all the references outside the pattern model (body of statements method) are automatically considered as parameters too. + * So there are parameters: start, next, end, statements. + */ + .build(); + } + + abstract String start(); + abstract String next(); + abstract String end(); + void statements() { + } + + interface Item {} + interface Entity { + Iterable $getItems$(); + } + class ListPrinter implements AutoCloseable { + @Override + public void close() throws Exception {} + public void printSeparatorIfAppropriate() {} + } + interface Printer { + ListPrinter createListPrinter(String p1, String p2, String p3); + } +} diff --git a/src/test/java/spoon/test/template/TemplateInvocationSubstitutionTest.java b/src/test/java/spoon/test/template/TemplateInvocationSubstitutionTest.java index 10b1f24f663..2991794b8fa 100644 --- a/src/test/java/spoon/test/template/TemplateInvocationSubstitutionTest.java +++ b/src/test/java/spoon/test/template/TemplateInvocationSubstitutionTest.java @@ -12,6 +12,7 @@ import spoon.support.compiler.FileSystemFile; import spoon.test.template.testclasses.InvocationSubstitutionByExpressionTemplate; import spoon.test.template.testclasses.InvocationSubstitutionByStatementTemplate; +import spoon.test.template.testclasses.SubstitutionByExpressionTemplate; public class TemplateInvocationSubstitutionTest { @@ -45,4 +46,18 @@ public void testInvocationSubstitutionByExpression() throws Exception { assertEquals("java.lang.System.out.println(\"abc\".substring(1))", result.getStatement(0).toString()); assertEquals("java.lang.System.out.println(\"abc\".substring(1))", result.getStatement(1).toString()); } + + @Test + public void testSubstitutionByExpression() throws Exception { + //contract: the template engine understands fields whose type extends from TemplateParameter as template parameter automatically. No need for extra annotation + Launcher spoon = new Launcher(); + spoon.addTemplateResource(new FileSystemFile("./src/test/java/spoon/test/template/testclasses/SubstitutionByExpressionTemplate.java")); + + spoon.buildModel(); + Factory factory = spoon.getFactory(); + + CtClass resultKlass = factory.Class().create("Result"); + CtBlock result = new SubstitutionByExpressionTemplate(factory.createLiteral("abc")).apply(resultKlass); + assertEquals("java.lang.System.out.println(\"abc\".substring(1))", result.getStatement(0).toString()); + } } diff --git a/src/test/java/spoon/test/template/TemplateTest.java b/src/test/java/spoon/test/template/TemplateTest.java index 3110bbe3200..0751134595a 100644 --- a/src/test/java/spoon/test/template/TemplateTest.java +++ b/src/test/java/spoon/test/template/TemplateTest.java @@ -4,6 +4,7 @@ import spoon.Launcher; import spoon.SpoonException; import spoon.compiler.SpoonResourceHelper; +import spoon.pattern.PatternBuilder; import spoon.reflect.code.CtBlock; import spoon.reflect.code.CtExpression; import spoon.reflect.code.CtForEach; @@ -26,6 +27,7 @@ import spoon.reflect.visitor.filter.NamedElementFilter; import spoon.support.compiler.FileSystemFile; import spoon.support.compiler.FileSystemFolder; +import spoon.support.gui.SpoonModelTree; import spoon.support.template.Parameters; import spoon.support.template.SubstitutionVisitor; import spoon.template.Substitution; @@ -538,6 +540,7 @@ public void testExtensionBlock() throws Exception { assertTrue(aTry.getBody().getStatement(0) instanceof CtInvocation); assertEquals("spoon.test.template.testclasses.logger.Logger.enter(\"Logger\", \"enter\")", aTry.getBody().getStatement(0).toString()); assertTrue(aTry.getBody().getStatements().size() > 1); + assertEquals("java.lang.System.out.println((((\"enter: \" + className) + \" - \") + methodName))", aTry.getBody().getStatement(1).toString()); } @Test @@ -561,7 +564,10 @@ public void testExtensionDecoupledSubstitutionVisitor() throws Exception { params.put("_classname_", factory.Code().createLiteral(aTargetType.getSimpleName())); params.put("_methodName_", factory.Code().createLiteral(toBeLoggedMethod.getSimpleName())); params.put("_block_", toBeLoggedMethod.getBody()); - final List> aMethods = new SubstitutionVisitor(factory, params).substitute(aTemplateModel.clone()); +// final List> aMethods = new SubstitutionVisitor(factory, params).substitute(aTemplateModel.clone()); + final List> aMethods = PatternBuilder.createPattern(aTemplateModel) + .configureAutomaticParameters() + .build().substitute(params); assertEquals(1, aMethods.size()); final CtMethod aMethod = aMethods.get(0); assertTrue(aMethod.getBody().getStatement(0) instanceof CtTry); @@ -1030,10 +1036,11 @@ public void substituteTypeAccessReference() throws Exception { Factory factory = spoon.getFactory(); //contract: String value is substituted in substring of literal, named element and reference - CtTypeReference typeRef = factory.Type().createReference(TypeReferenceClassAccessTemplate.Example.class); + CtTypeReference typeRef = factory.Type().createReference("spoon.test.template.TypeReferenceClassAccess$Example"); typeRef.addActualTypeArgument(factory.Type().DATE); final CtClass result = (CtClass) new TypeReferenceClassAccessTemplate(typeRef).apply(factory.Class().create("spoon.test.template.TypeReferenceClassAccess")); +// new SpoonModelTree(result.getFactory()); spoon.prettyprint(); ModelUtils.canBeBuilt(outputDir, 8); CtMethod method = result.getMethodsByName("someMethod").get(0); diff --git a/src/test/java/spoon/test/template/testclasses/SubstitutionByExpressionTemplate.java b/src/test/java/spoon/test/template/testclasses/SubstitutionByExpressionTemplate.java new file mode 100644 index 00000000000..52180bbe6c8 --- /dev/null +++ b/src/test/java/spoon/test/template/testclasses/SubstitutionByExpressionTemplate.java @@ -0,0 +1,21 @@ +package spoon.test.template.testclasses; + +import spoon.reflect.code.CtExpression; +import spoon.template.BlockTemplate; +import spoon.template.Local; + +public class SubstitutionByExpressionTemplate extends BlockTemplate { + + @Override + public void block() throws Throwable { + System.out.println(_expression_.S().substring(1)); + } + + //note that there is no @Parameter annotation! This field is detected as template parameter, because it's type extends TemplateParameter + CtExpression _expression_; + + @Local + public SubstitutionByExpressionTemplate(CtExpression expr) { + this._expression_ = expr; + } +} \ No newline at end of file diff --git a/src/test/java/spoon/test/template/testclasses/inheritance/InterfaceTemplate.java b/src/test/java/spoon/test/template/testclasses/inheritance/InterfaceTemplate.java index e0f2e60618e..33f08e067df 100644 --- a/src/test/java/spoon/test/template/testclasses/inheritance/InterfaceTemplate.java +++ b/src/test/java/spoon/test/template/testclasses/inheritance/InterfaceTemplate.java @@ -3,6 +3,7 @@ import spoon.reflect.factory.Factory; import spoon.reflect.reference.CtTypeReference; import spoon.template.ExtensionTemplate; +import spoon.template.Local; import spoon.template.Parameter; import java.io.Serializable;